libcore.net.http.RawHeaders Maven / Gradle / Ivy
/*
* 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 libcore.net.http;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import libcore.util.Libcore;
/**
* The HTTP status and unparsed header fields of a single HTTP message. Values
* are represented as uninterpreted strings; use {@link RequestHeaders} and
* {@link ResponseHeaders} for interpreted headers. This class maintains the
* order of the header fields within the HTTP message.
*
* This class tracks fields 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.
*/
public final class RawHeaders {
private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() {
// @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
@Override public int compare(String a, String b) {
if (a == b) {
return 0;
} else if (a == null) {
return -1;
} else if (b == null) {
return 1;
} else {
return String.CASE_INSENSITIVE_ORDER.compare(a, b);
}
}
};
private final List namesAndValues = new ArrayList(20);
private String statusLine;
private int httpMinorVersion = 1;
private int responseCode = -1;
private String responseMessage;
public RawHeaders() {}
public RawHeaders(RawHeaders copyFrom) {
namesAndValues.addAll(copyFrom.namesAndValues);
statusLine = copyFrom.statusLine;
httpMinorVersion = copyFrom.httpMinorVersion;
responseCode = copyFrom.responseCode;
responseMessage = copyFrom.responseMessage;
}
/**
* Sets the response status line (like "HTTP/1.0 200 OK") or request line
* (like "GET / HTTP/1.1").
*/
public void setStatusLine(String statusLine) {
statusLine = statusLine.trim();
this.statusLine = statusLine;
if (statusLine == null || !statusLine.startsWith("HTTP/")) {
return;
}
statusLine = statusLine.trim();
int mark = statusLine.indexOf(" ") + 1;
if (mark == 0) {
return;
}
if (statusLine.charAt(mark - 2) != '1') {
this.httpMinorVersion = 0;
}
int last = mark + 3;
if (last > statusLine.length()) {
last = statusLine.length();
}
this.responseCode = Integer.parseInt(statusLine.substring(mark, last));
if (last + 1 <= statusLine.length()) {
this.responseMessage = statusLine.substring(last + 1);
}
}
public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
String status = null;
String version = null;
for (int i = 0; i < namesAndValues.size(); i += 2) {
String name = namesAndValues.get(i);
if (name.equals("status")) {
status = namesAndValues.get(i + 1);
} else if (name.equals("version")) {
version = namesAndValues.get(i + 1);
}
}
if (status == null || version == null) {
throw new IOException("Expected 'status' and 'version' headers not present");
}
setStatusLine(version + " " + status);
}
/**
* @param method like "GET", "POST", "HEAD", etc.
* @param scheme like "https"
* @param url like "/foo/bar.html"
* @param version like "HTTP/1.1"
*/
public void addSpdyRequestHeaders(String method, String scheme, String url, String version) {
// TODO: populate the statusLine for the client's benefit?
add("method", method);
add("scheme", scheme);
add("url", url);
add("version", version);
}
public String getStatusLine() {
return statusLine;
}
/**
* Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
* and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
*/
public int getHttpMinorVersion() {
return httpMinorVersion != -1 ? httpMinorVersion : 1;
}
/**
* Returns the HTTP status code or -1 if it is unknown.
*/
public int getResponseCode() {
return responseCode;
}
/**
* Returns the HTTP status message or null if it is unknown.
*/
public String getResponseMessage() {
return responseMessage;
}
/**
* Add an HTTP header line containing a field name, a literal colon, and a
* value.
*/
public void addLine(String line) {
int index = line.indexOf(":");
if (index == -1) {
add("", line);
} else {
add(line.substring(0, index), line.substring(index + 1));
}
}
/**
* Add a field with the specified value.
*/
public void add(String fieldName, String value) {
if (fieldName == null) {
throw new IllegalArgumentException("fieldName == null");
}
if (value == null) {
/*
* Given null values, the RI sends a malformed field line like
* "Accept\r\n". For platform compatibility and HTTP compliance, we
* print a warning and ignore null values.
*/
Libcore.logW("Ignoring HTTP header field '" + fieldName + "' because its value is null");
return;
}
namesAndValues.add(fieldName);
namesAndValues.add(value.trim());
}
public void 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
}
}
}
public void addAll(String fieldName, List headerFields) {
for (String value : headerFields) {
add(fieldName, value);
}
}
/**
* 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 void set(String fieldName, String value) {
removeAll(fieldName);
add(fieldName, value);
}
/**
* Returns the number of field values.
*/
public int length() {
return namesAndValues.size() / 2;
}
/**
* Returns the field at {@code position} or null if that is out of range.
*/
public String getFieldName(int index) {
int fieldNameIndex = index * 2;
if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
return null;
}
return namesAndValues.get(fieldNameIndex);
}
/**
* Returns the value at {@code index} or null if that is out of range.
*/
public String getValue(int index) {
int valueIndex = index * 2 + 1;
if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
return null;
}
return namesAndValues.get(valueIndex);
}
/**
* Returns the last value corresponding to the specified field, or null.
*/
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;
}
/**
* @param fieldNames a case-insensitive set of HTTP header field names.
*/
public RawHeaders getAll(Set fieldNames) {
RawHeaders result = new RawHeaders();
for (int i = 0; i < namesAndValues.size(); i += 2) {
String fieldName = namesAndValues.get(i);
if (fieldNames.contains(fieldName)) {
result.add(fieldName, namesAndValues.get(i + 1));
}
}
return result;
}
public String toHeaderString() {
StringBuilder result = new StringBuilder(256);
result.append(statusLine).append("\r\n");
for (int i = 0; i < namesAndValues.size(); i += 2) {
result.append(namesAndValues.get(i)).append(": ")
.append(namesAndValues.get(i + 1)).append("\r\n");
}
result.append("\r\n");
return result.toString();
}
/**
* Returns an immutable map containing each field to its list of values. The
* status line is mapped to null.
*/
public Map> toMultimap() {
Map> result = new TreeMap>(FIELD_NAME_COMPARATOR);
for (int i = 0; i < namesAndValues.size(); i += 2) {
String fieldName = namesAndValues.get(i);
String value = namesAndValues.get(i + 1);
List allValues = new ArrayList();
List otherValues = result.get(fieldName);
if (otherValues != null) {
allValues.addAll(otherValues);
}
allValues.add(value);
result.put(fieldName, Collections.unmodifiableList(allValues));
}
if (statusLine != null) {
result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
}
return Collections.unmodifiableMap(result);
}
/**
* Creates a new instance from the given map of fields to values. If
* present, the null field's last element will be used to set the status
* line.
*/
public static RawHeaders fromMultimap(Map> map) {
RawHeaders result = new RawHeaders();
for (Entry> entry : map.entrySet()) {
String fieldName = entry.getKey();
List values = entry.getValue();
if (fieldName != null) {
result.addAll(fieldName, values);
} else if (!values.isEmpty()) {
result.setStatusLine(values.get(values.size() - 1));
}
}
return result;
}
/**
* Returns a list of alternating names and values. Names are all lower case.
* No names are repeated. If any name has multiple values, they are
* concatenated using "\0" as a delimiter.
*/
public List toNameValueBlock() {
Set names = new HashSet();
List result = new ArrayList();
for (int i = 0; i < namesAndValues.size(); i += 2) {
String name = namesAndValues.get(i).toLowerCase(Locale.US);
String value = namesAndValues.get(i + 1);
// TODO: promote this check to where names and values are created
if (name.isEmpty() || value.isEmpty()
|| name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
}
// If we haven't seen this name before, add the pair to the end of the list...
if (names.add(name)) {
result.add(name);
result.add(value);
continue;
}
// ...otherwise concatenate the existing values and this value.
for (int j = 0; j < result.size(); j += 2) {
if (name.equals(result.get(j))) {
result.set(j + 1, result.get(j + 1) + "\0" + value);
break;
}
}
}
return result;
}
public static RawHeaders fromNameValueBlock(List nameValueBlock) {
if (nameValueBlock.size() % 2 != 0) {
throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
}
RawHeaders result = new RawHeaders();
for (int i = 0; i < nameValueBlock.size(); i += 2) {
String name = nameValueBlock.get(i);
String values = nameValueBlock.get(i + 1);
for (int start = 0; start < values.length(); ) {
int end = values.indexOf(start, '\0');
if (end == -1) {
end = values.length();
}
result.namesAndValues.add(name);
result.namesAndValues.add(values.substring(start, end));
start = end + 1;
}
}
return result;
}
}