FromServer.java
/**
* Copyright (C) 2017 HttpBuilder-NG Project
*
* 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.
*/
package groovyx.net.http;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpCookie;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiFunction;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static java.util.Arrays.stream;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
/**
* Adapter interface used to provide a bridge for response data between the {@link HttpBuilder} API and the underlying client implementation.
*/
public interface FromServer {
public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
/**
* Defines the interface to the HTTP headers contained in the response. (see also
* https://en.wikipedia.org/wiki/List_of_HTTP_header_fields[List of HTTP Header Fields])
*/
public static abstract class Header<T> implements Map.Entry<String, String> {
final String key;
final String value;
private T parsed;
protected static String key(final String raw) {
return raw.substring(0, raw.indexOf(':')).trim();
}
protected static String cleanQuotes(final String str) {
return str.startsWith("\"") ? str.substring(1, str.length() - 1) : str;
}
protected static String value(final String raw) {
return cleanQuotes(raw.substring(raw.indexOf(':') + 1).trim());
}
protected Header(final String key, final String value) {
this.key = key;
this.value = value;
}
/**
* Retrieves the header `key`.
*
* @return the header key
*/
public String getKey() {
return key;
}
/**
* Retrieves the header `value`.
*
* @return the header value
*/
public String getValue() {
return value;
}
/**
* Unsupported, headers are read-only.
*
* @throws UnsupportedOperationException always
*/
public String setValue(final String val) {
throw new UnsupportedOperationException();
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof Header)) {
return false;
}
Header other = (Header) o;
return (Objects.equals(getKey(), other.getKey()) &&
Objects.equals(getValue(), other.getValue()));
}
@Override
public int hashCode() {
return Objects.hash(getKey(), getValue());
}
@Override
public String toString() {
return key + ": " + value;
}
/**
* Retrieves the parsed representation of the 'value`. The type of
* the returned `Object` depends on the header and will be given
* by the `getParsedType()` property.
*
* @return the parsed header value
*/
public T getParsed() {
if (parsed == null) {
this.parsed = parse();
}
return parsed;
}
/**
* Retrieves the type of the parsed representation of the 'value`.
*
* @return the parsed header value type
*/
public abstract Class<?> getParsedType();
/**
* Performs the parse of the `value`
*
* @return the parsed header value
*/
protected abstract T parse();
/**
* Creates a `Header` from a full header string. The string format is colon-delimited as `KEY:VALUE`.
*
* [source,groovy]
* ----
* Header header = Header.full('Content-Type:text/plain')
* assert header.key == 'Content-Type'
* assert header.value == 'text/plain'
* ----
*
* @param raw the full header string
* @return the `Header` representing the given header string
*/
public static Header<?> full(final String raw) {
return keyValue(key(raw), value(raw));
}
/**
* Creates a `Header` from a given `key` and `value`.
*
* @param key the header key
* @param value the header value
* @return the populated `Header`
*/
public static Header<?> keyValue(String key, String value) {
final BiFunction<String, String, ? extends Header> func = constructors.get(key);
return func == null ? new ValueOnly(key, value) : func.apply(key, value);
}
/**
* Used to find a specific `Header` by key from a {@link Collection} of `Header`s.
*
* @param headers the {@link Collection} of `Header`s to be searched
* @param key the key of the desired `Header`
* @return the `Header` with the matching key (or `null`)
*/
public static Header<?> find(final Collection<Header<?>> headers, final String key) {
return headers.stream().filter((h) -> h.getKey().equalsIgnoreCase(key)).findFirst().orElse(null);
}
/**
* Type representing headers that are simple key/values, with no parseable structure in the value. For example: `Accept-Ranges: bytes`.
*/
public static class ValueOnly extends Header<String> {
public ValueOnly(final String key, final String value) {
super(key, value);
}
public String parse() {
return getValue();
}
/**
* Always returns {@link String}
*
* @return the parsed header type
*/
public Class<?> getParsedType() {
return String.class;
}
}
/**
* Type representing headers that have values which are parseable as key/value pairs,
* provided the header hey is included in the key/value map.
* For example: `Content-Type: text/html; charset=utf-8`
*/
public static class CombinedMap extends Header<Map<String, String>> {
public CombinedMap(final String key, final String value) {
super(key, value);
}
public Map<String, String> parse() {
Map<String, String> ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
final String[] ary = getValue().split(";");
ret.put(key, cleanQuotes(ary[0].trim()));
if (ary.length > 1) {
final String[] secondary = ary[1].split("=");
ret.put(secondary[0].trim(), cleanQuotes(secondary[1].trim()));
}
return unmodifiableMap(ret);
}
/**
* Always returns {@link List}
*
* @return the parsed header type
*/
public Class<?> getParsedType() {
return Map.class;
}
}
/**
* Type representing headers that have values which are comma separated lists.
* For example: `Allow: GET, HEAD`
*/
public static class CsvList extends Header<List<String>> {
public CsvList(final String key, final String value) {
super(key, value);
}
public List<String> parse() {
return unmodifiableList(stream(getValue().split(",")).map(String::trim).collect(toList()));
}
public Class<?> getParsedType() {
return List.class;
}
}
/**
* Type representing headers that have values which are zoned date time values.
* Values representing seconds from now are also converted to zoned date time values
* with UTC/GMT zone offsets.
*
* * Example 1: `Retry-After: Fri, 07 Nov 2014 23:59:59 GMT`
* * Example 2: `Retry-After: 120`
*/
public static class HttpDate extends Header<ZonedDateTime> {
public HttpDate(final String key, final String value) {
super(key, value);
}
private boolean isSimpleNumber() {
for (int i = 0; i < getValue().length(); ++i) {
if (!Character.isDigit(getValue().charAt(i))) {
return false;
}
}
return true;
}
/**
* Always returns {@link ZonedDateTime}
*
* @return the parsed header type
*/
public ZonedDateTime parse() {
if (isSimpleNumber()) {
return ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(Long.parseLong(getValue()));
} else {
return parse(RFC_1123_DATE_TIME);
}
}
/**
* Retrieves the {@link ZonedDateTime} value of the header using the provided {@link DateTimeFormatter}.
*
* @param formatter the formatter to be used
* @return
*/
public ZonedDateTime parse(final DateTimeFormatter formatter) {
return ZonedDateTime.parse(getValue(), formatter);
}
public Class<?> getParsedType() {
return ZonedDateTime.class;
}
}
/**
* Type representing headers that have values which are parseable as key/value pairs.
* For example: `Alt-Svc: h2="http2.example.com:443"; ma=7200`
*/
public static class MapPairs extends Header<Map<String, String>> {
public MapPairs(final String key, final String value) {
super(key, value);
}
public Map<String, String> parse() {
return stream(getValue().split(";"))
.map(String::trim)
.map((str) -> str.split("="))
.collect(toMap((ary) -> ary[0].trim(),
(ary) -> {
if (ary.length == 1) {
return ary[0];
} else {
return cleanQuotes(ary[1].trim());
}
},
(oldVal, newVal) -> newVal,
() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
}
/**
* Always returns {@link Map}
*
* @return the parsed header type
*/
public Class<?> getParsedType() {
return Map.class;
}
}
/**
* Type representing headers that have values which are parseable as longs.
* For example: `Content-Length: 348`
*/
public static class SingleLong extends Header<Long> {
public SingleLong(final String key, final String value) {
super(key, value);
}
public Long parse() {
return Long.valueOf(getValue());
}
/**
* Always returns {@link Long}
*
* @return the parsed header type
*/
public Class<?> getParsedType() {
return Long.class;
}
}
public static class HttpCookies extends Header<List<HttpCookie>> {
public HttpCookies(final String key, final String value) {
super(key, value);
}
public List<HttpCookie> parse() {
return HttpCookie.parse(key + ": " + value);
}
public Class<?> getParsedType() {
return List.class;
}
}
private static final Map<String, BiFunction<String, String, ? extends Header>> constructors;
static {
final Map<String, BiFunction<String, String, ? extends Header>> tmp = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
tmp.put("Access-Control-Allow-Origin", ValueOnly::new);
tmp.put("Accept-Patch", CombinedMap::new);
tmp.put("Accept-Ranges", ValueOnly::new);
tmp.put("Age", SingleLong::new);
tmp.put("Allow", CsvList::new);
tmp.put("Alt-Svc", MapPairs::new);
tmp.put("Cache-Control", MapPairs::new);
tmp.put("Connection", ValueOnly::new);
tmp.put("Content-Disposition", CombinedMap::new);
tmp.put("Content-Encoding", ValueOnly::new);
tmp.put("Content-Language", ValueOnly::new);
tmp.put("Content-Length", SingleLong::new);
tmp.put("Content-Location", ValueOnly::new);
tmp.put("Content-MD5", ValueOnly::new);
tmp.put("Content-Range", ValueOnly::new);
tmp.put("Content-Type", CombinedMap::new);
tmp.put("Date", HttpDate::new);
tmp.put("ETag", ValueOnly::new);
tmp.put("Expires", HttpDate::new);
tmp.put("Last-Modified", HttpDate::new);
tmp.put("Link", CombinedMap::new);
tmp.put("Location", ValueOnly::new);
tmp.put("P3P", MapPairs::new);
tmp.put("Pragma", ValueOnly::new);
tmp.put("Proxy-Authenticate", ValueOnly::new);
tmp.put("Public-Key-Pins", MapPairs::new);
tmp.put("Refresh", CombinedMap::new);
tmp.put("Retry-After", HttpDate::new);
tmp.put("Server", ValueOnly::new);
tmp.put("Set-Cookie", HttpCookies::new);
tmp.put("Set-Cookie2", HttpCookies::new);
tmp.put("Status", ValueOnly::new);
tmp.put("Strict-Transport-Security", MapPairs::new);
tmp.put("Trailer", ValueOnly::new);
tmp.put("Transfer-Encoding", ValueOnly::new);
tmp.put("TSV", ValueOnly::new);
tmp.put("Upgrade", CsvList::new);
tmp.put("Vary", ValueOnly::new);
tmp.put("Via", CsvList::new);
tmp.put("Warning", ValueOnly::new);
tmp.put("WWW-Authenticate", ValueOnly::new);
tmp.put("X-Frame-Options", ValueOnly::new);
constructors = unmodifiableMap(tmp);
}
}
/**
* Retrieves the value of the "Content-Type" header from the response.
*
* @return the value of the "Content-Type" response header
*/
default String getContentType() {
final Header.CombinedMap header = (Header.CombinedMap) Header.find(getHeaders(), "Content-Type");
if (header == null) {
return DEFAULT_CONTENT_TYPE;
} else {
return header.getParsed().get("Content-Type");
}
}
/**
* Retrieves the value of the charset from the "Content-Type" response header.
*
* @return the value of the charset from the "Content-Type" response header
*/
default Charset getCharset() {
final Header.CombinedMap header = (Header.CombinedMap) Header.find(getHeaders(), "Content-Type");
if (header == null) {
return StandardCharsets.UTF_8;
}
if (header.getParsed().containsKey("charset")) {
Charset.forName(header.getParsed().get("charset"));
}
return StandardCharsets.UTF_8;
}
default List<HttpCookie> getCookies() {
return HttpBuilder.cookies(getHeaders());
}
/**
* Retrieves the {@link InputStream} containing the response content (may have already been processed).
*
* @return the response content
*/
InputStream getInputStream();
/**
* Retrieves the response status code (https://en.wikipedia.org/wiki/List_of_HTTP_status_codes[List of HTTP status code]).
*
* @return the response status code
*/
int getStatusCode();
/**
* Retrieves the response status message.
*
* @return the response status message (or null)
*/
String getMessage();
/**
* Retrieves a {@link List} of the response headers as ({@link Header} objects).
*
* @return a {@link List} of response headers
*/
List<Header<?>> getHeaders();
/**
* Determines whether or not there is body content in the response.
*
* @return true if there is body content in the response
*/
boolean getHasBody();
/**
* Retrieves the {@link URI} of the original request.
*
* @return the {@link URI} of the original request
*/
URI getUri();
/**
* Performs any client-specific response finishing operations.
*/
void finish();
/**
* Retrieves a {@link Reader} for the response body content (if there is any). The content may have already been processed.
*
* @return a {@link Reader} for the response body content (may be empty)
*/
default Reader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}