Java tutorial
/* Copyright (C) 2013-2014 Ian Teune <ian.teune@gmail.com> * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.tinspx.util.net; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ascii; import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Optional; import static com.google.common.base.Preconditions.*; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ForwardingList; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.SetMultimap; import com.google.common.net.HttpHeaders; import com.google.common.net.MediaType; import com.google.common.primitives.Longs; import com.tinspx.util.collect.Listenable; import com.tinspx.util.collect.Predicated; import com.tinspx.util.io.CAWriter; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import lombok.EqualsAndHashCode; import lombok.NonNull; import lombok.ToString; /** * Wraps a {@link ListMultimap} as collection of headers. The iteration order of * the headers is preserved. All polling methods of this class * ({@link #get(String) get}, {@link #contains(String) contains}, etc...) are * all case insensitive. However, the multimap and map return from * {@link #asMultimap()} and {@link #asMap()} represent the raw header strings * and <i>are</i> case sensitive. * <p> * {@link #parseContentType(CharSequence) parseContentType} and * {@link #parseMediaType(CharSequence) parseMediaType} can be used to parse * {@code Content-Type} headers. * * @author Ian */ @NotThreadSafe @ToString(of = { "normalize", "headers" }) @EqualsAndHashCode(of = "headers") public class Headers implements Iterable<Map.Entry<String, String>> { /** * raw header values. */ private ListMultimap<String, String> headers; /** * maps a canonical name to its actual raw header names. */ @Nullable @VisibleForTesting SetMultimap<String, String> canons; /** * true when {@link #canons} needs to be rebuilt, typically because * {@link #headers} was directly modified. */ private boolean rebuildCanons; /** * same as {@link #headers}, except with canonical names. */ @Nullable @VisibleForTesting ListMultimap<String, String> canonHeaders; /** * true when {@link #canonHeaders} needs to be rebuilt, typically because * {@link #headers} was directly modified. */ private boolean rebuildHeaders; /** * a modifiable view of {@link #headers} returned to the user. does not * allow null keys or values. */ private @Nullable ListMultimap<String, String> view; private ListMultimap<String, String> view() { if (view == null) { view = Predicated.listMultimap(Listenable.listMultimap(headers, true, new Listenable.Modification() { @Override public void onModify(Object src, Listenable.Event type) { rebuildCanons = true; rebuildHeaders = true; } }), Predicates.notNull(), Predicates.notNull(), true); } return view; } /** * if true, header names are {@link #normalize(String) normalized}. */ private boolean normalize; public Headers() { this.headers = LinkedListMultimap.create(); } public Headers(Headers headers) { normalize = headers.normalize; this.headers = LinkedListMultimap.create(Math.max(8, headers.headers.keySet().size())); addAll(headers.headers.entries()); } private ListMultimap<String, String> canonHeaders() { if (canonHeaders == null || rebuildHeaders) { if (canonHeaders != null) { canonHeaders.clear(); } else { canonHeaders = LinkedListMultimap.create(); } for (Map.Entry<String, String> h : headers.entries()) { canonHeaders.put(canonicalize(h.getKey()), h.getValue()); } rebuildHeaders = false; } return canonHeaders; } private SetMultimap<String, String> canons() { if (canons == null || rebuildCanons) { if (canons != null) { canons.clear(); } else { canons = HashMultimap.create(); } for (String name : headers.keySet()) { canons.put(canonicalize(name), name); } rebuildCanons = false; } return canons; } /** * Determines if header names are {@link #normalize(String) normalized}. * Defaults to {@code false}. Note that headers will not be normalized if * they are added directly to the underlying {@link #asMultimap() multimap} * or {@link #asMap() map}, regardless of this setting. */ public boolean isNormalize() { return normalize; } /** * Sets whether header names are {@link #normalize(String) normalized}. * Defaults to {@code false}. Note that headers will not be normalized if * they are added directly to the underlying {@link #asMultimap() multimap}, * regardless of this setting. */ public Headers setNormalize(boolean normalize) { if (normalize && !this.normalize && !headers.isEmpty()) { final ListMultimap<String, String> h = LinkedListMultimap.create(Math.max(8, headers.keySet().size())); for (Map.Entry<String, String> e : headers.entries()) { h.put(normalize(e.getKey()), e.getValue()); } headers = h; view = null; rebuildCanons = true; rebuildHeaders = true; } this.normalize = normalize; return this; } /** * Returns a modifiable {@code ListMultimap} view of the headers. */ public ListMultimap<String, String> asMultimap() { return view(); } /** * Returns a modifiable {@code Map} view of the headers. Note that this map * is acquired through {@link Multimap#asMap()}, and therefore the map does * not support {@code put} or {@code putAll}, nor do its entries support * {@link java.util.Map.Entry#setValue(Object) setValue}. */ public Map<String, List<String>> asMap() { return Multimaps.asMap(view()); } @Override public Iterator<Map.Entry<String, String>> iterator() { return view().entries().iterator(); } public boolean isEmpty() { return headers.isEmpty(); } /** * Removes all headers associated with the specified header {@code name}. */ public Headers removeAll(@Nullable String name) { if (name == null) { return this; } name = canonicalize(name); for (String header : canons().get(name)) { headers.removeAll(header); } canons().removeAll(name); if (canonHeaders != null && !rebuildHeaders) { canonHeaders.removeAll(name); } return this; } /** * Removes the header {@code name/value} pair. Other values associated with * {@code name} are not removed. */ public Headers remove(@Nullable String name, @Nullable String value) { if (name == null || value == null) { return this; } name = canonicalize(name); final Set<String> singleton = Collections.singleton(value); for (String header : canons().get(name)) { headers.get(header).removeAll(singleton); } if (canonHeaders != null && !rebuildHeaders) { canonHeaders.get(name).removeAll(singleton); } return this; } /** * Replaces all existing headers with the specified {@code name} with * {@code value}. Equivalent to {@code removeAll(name).add(name, value)}, * except iteration order will be preserved if possible. * * @throws NullPointerException if either {@code name} or {@code value} * is {@code null} */ public Headers set(@NonNull String name, @NonNull String value) { if (normalize) { name = normalize(name); } final Set<String> singleton = Collections.singleton(value); headers.replaceValues(name, singleton); final String cn = canonicalize(name); for (String header : canons().get(cn)) { if (!header.equals(name)) { headers.removeAll(header); } } canons().replaceValues(cn, Collections.singleton(name)); if (canonHeaders != null && !rebuildHeaders) { canonHeaders.replaceValues(cn, singleton); } return this; } /** * Adds the specified header {@code name/value} pair. * * @throws NullPointerException if either {@code name} or {@code value} * is {@code null} */ public Headers add(@NonNull String name, @NonNull String value) { if (normalize) { name = normalize(name); } headers.put(name, value); String cn = null; if (canons != null && !rebuildCanons) { canons.put(cn = canonicalize(name), name); } if (canonHeaders != null && !rebuildHeaders) { canonHeaders.put(cn != null ? cn : canonicalize(name), value); } return this; } public Headers addAll(@NonNull Headers headers) { return addAll(headers.headers); } public Headers addAll(@NonNull Map<String, ? extends Iterable<String>> headers) { for (Map.Entry<String, ? extends Iterable<String>> e : headers.entrySet()) { addAll(e.getKey(), e.getValue()); } return this; } public Headers addAllFrom(@NonNull Map<String, String> headers) { return addAll(headers.entrySet()); } public Headers addAll(@NonNull Multimap<String, String> headers) { return addAll(headers.entries()); } private Headers addAll(Iterable<Map.Entry<String, String>> entries) { for (Map.Entry<String, String> h : entries) { add(h.getKey(), h.getValue()); } return this; } public Headers addAll(@NonNull String name, String... values) { return addAll(name, Arrays.asList(values)); } public Headers addAll(@NonNull String name, Iterable<String> values) { for (String value : values) { add(name, value); } return this; } /** * Returns all values mapped to the specified header {@code name}. The * returned list is a "live" view of the headers mapped to {@code name}, so * any values added or removed with the given header {@code name} will be * reflected in the returned list. However, the returned list is <i>not</i> * modifiable. */ public List<String> get(@Nullable String name) { if (name == null) { return Collections.emptyList(); } final List<String> delegate = canonHeaders().get(canonicalize(name)); return Collections.unmodifiableList(new ForwardingList<String>() { @Override protected List<String> delegate() { if (rebuildHeaders) { canonHeaders(); } return delegate; } }); } /** * same as {@link #get(String)}, except not wrapped in an * unmodifiable/forwarding list. */ private List<String> getImpl(@Nullable String name) { if (name == null) { return Collections.emptyList(); } return canonHeaders().get(canonicalize(name)); } /** * Returns the <i>first</i> header value mapped to {@code name}, or the * the empty {@code String} if there are none. */ public String first(@Nullable String name) { final List<String> h = getImpl(name); return h.isEmpty() ? "" : Strings.nullToEmpty(h.get(0)); } /** * Returns the <i>last</i> header value mapped to {@code name}, or the * the empty {@code String} if there are none. */ public String last(@Nullable String name) { final List<String> h = getImpl(name); return h.isEmpty() ? "" : Strings.nullToEmpty(h.get(h.size() - 1)); } /** * Returns the number of header values associated with the header * {@code name}. */ public int count(@Nullable String name) { return getImpl(name).size(); } /** * Returns the total number of headers. Equivalent to * {@code asMultimap().size()}. */ public int size() { return headers.size(); } /** * Determines if there is at least one value associated with the specified * header {@code name}. */ public boolean contains(@Nullable String name) { if (name == null) { return false; } return headers.containsKey(name) || canonHeaders().containsKey(canonicalize(name)); } public boolean contains(@Nullable String name, @Nullable String value) { if (name == null || value == null) { return false; } return headers.containsEntry(name, value) || canonHeaders().containsEntry(canonicalize(name), value); } public boolean containsAny(String... names) { return containsAny(Arrays.asList(names)); } public boolean containsAny(Iterable<String> names) { for (String name : names) { if (contains(name)) { return true; } } return false; } public boolean containsAll(String... names) { return containsAll(Arrays.asList(names)); } public boolean containsAll(Iterable<String> names) { for (String name : names) { if (!contains(name)) { return false; } } return true; } /** * Determines if the headers contains the specified name/value pair. * However, {@code valueFunction} is used to transform all header values * before checking for equality. {@code valueFunction} is applied to * {@code value} before searching. */ public boolean contains(@Nullable String name, @Nullable String value, @NonNull Function<? super String, ? extends String> valueFunction) { if (name == null) { //can't check value==null as the function will affect the nullity //of the header values return false; } value = valueFunction.apply(value); for (final String hvalue : getImpl(name)) { if (Objects.equal(value, valueFunction.apply(hvalue))) { return true; } } return false; } /** * Determines if the headers contain a value mapped to {@code name} that * satisfies {@code valuePredicate}. */ public boolean contains(@Nullable String name, @NonNull Predicate<? super String> valuePredicate) { if (name == null) { return false; } for (final String value : getImpl(name)) { if (valuePredicate.apply(value)) { return true; } } return false; } /** * Removes the specified name/value pair. However, {@code valueFunction} is * used to transform all header values before checking for equality, which * may result in multiple headers being removed. {@code valueFunction} is * applied to {@code value} before searching. */ public Headers remove(@Nullable String name, @Nullable String value, @NonNull Function<? super String, ? extends String> valueFunction) { if (name == null) { //can't check value==null as the function will affect the nullity //of the header values return this; } value = valueFunction.apply(value); for (final String header : canons().get(canonicalize(name))) { final List<String> values = headers.get(header); if (values.isEmpty()) { continue; } final Iterator<String> viter = values.iterator(); while (viter.hasNext()) { if (Objects.equal(value, valueFunction.apply(viter.next()))) { viter.remove(); rebuildHeaders = true; } } } return this; } /** * Removes all headers mapped to {@code name} that satisfy * {@code valuePredicate}. */ public Headers remove(@Nullable String name, @NonNull Predicate<? super String> valuePredicate) { if (name == null) { return this; } for (final String header : canons().get(canonicalize(name))) { final List<String> values = headers.get(header); if (values.isEmpty()) { continue; } if (Iterables.removeIf(values, valuePredicate)) { rebuildHeaders = true; } } return this; } /** * Returns the value of the {@code Content-Length} header, or 0 if the * header is missing or invalid. Will not throw a * {@code NumberFormatException}. * * @see #parseContentLength() */ public long contentLength() { final String header = last(HttpHeaders.CONTENT_LENGTH).trim(); if (header.isEmpty()) { return 0; } else { final Long len = Longs.tryParse(header); return len != null ? Math.max(0, len) : 0; } } /** * Parses a the value of the {@code Content-Length} header, throwing a * {@code NumberFormatException} if missing or invalid. * * @see #contentLength() */ public long parseContentLength() { final long len = Long.parseLong(last(HttpHeaders.CONTENT_LENGTH).trim()); if (len < 0) { throw new NumberFormatException("Content-Length cannot be negative: " + len); } return len; } /** * Returns the value of the {@code Content-Type} header or the empty string * if missing. */ public String contentType() { return last(HttpHeaders.CONTENT_TYPE); } /** * Sets the value of the {@code Content-Type} header. */ public Headers contentType(@NonNull String contentType) { return set(HttpHeaders.CONTENT_TYPE, contentType); } /** * <i>very</i> leniently parses a Content-Type header as defined by * <a href="http://tools.ietf.org/html/rfc2045#section-5.1">RFC 2045</a> * and returns it as a {@link MediaType}. * <p> * See {@link #parseContentType(CharSequence)} for more * information on how {@code contentType} is parsed. If the type/subtype of * the media type is not available in {@code contentType}, this method will * throw an {@code IllegalArgumentException}. * * @param contentType the Content-Type header field to parse * @return the parsed {@code MediaType} * @throws IllegalArgumentException if the type/subtype information is not * present in {@code contentType} */ public static MediaType parseMediaType(CharSequence contentType) { LenientContentType lct = new LenientContentType(contentType); lct.parse(); return lct.convert(); } /** * <i>very</i> leniently parses a Content-Type header as defined by * <a href="http://tools.ietf.org/html/rfc2045#section-5.1">RFC 2045</a>. * <p> * And when I say leniently, I mean this method will try to parse any mess * thrown at it. The {@code contentType} does not have to have the leading * string "Content-Type:". The field name does not have to be Content-Type * and may be missing the trailing colon as long as there is some other * delimiting characters. The media type may have no type or subtype or may * be missing all together. There may be any number of parameters, and every * parameter may be invalid. Invalid parameters are skipped/ignored. * Whitespace and comments are skipped/ignored anywhere delimiters are * allowed. Improperly nested/terminated comments will cause the all * subsequent characters to be skipped. Inside of quoted-string values, * "CRLF LWSP-char" linear-white-space that requires line folding is * stripped from the value unless the "CRLF LWSP-char" linear-white-space is * quoted with a preceding backslash ("\"), in which case "CRLF" is kept but * the "LWSP-char" char is stripped. * <p> * All tokens (type, subtype, attribute) are normalized to lowercase. * attribute values are case-sensitive and are not lowercased except for the * "charset" attribute (this is done to mimick behavior of guava * {@link MediaType}). * <p> * Most properties of the returned {@link ContentType} are optional due to * the leniency of this method. This method will never fail and always * returns a non-null value. However, the returned {@code ContentType} * may be empty. * * @param contentType the Content-Type header field to parse * @return the parsed {@code ContentType} */ public static ContentType parseContentType(CharSequence contentType) { LenientContentType lct = new LenientContentType(contentType); lct.parse(); return lct; } /** * Represents a Content-Type as defined by * <a href="http://tools.ietf.org/html/rfc2045#section-5.1">RFC 2045</a>. * This interface is intended for parsing bad Content-Type header fields * and all parts of the Content-Type are optional. */ public interface ContentType { /** * Returns the original Content-Type string source used to parse this * {@code ContentType}. */ CharSequence source(); Optional<String> type(); Optional<String> subtype(); ImmutableListMultimap<String, String> parameters(); /** * If both {@link #type()} and {@link #subtype()} are present, a * {@link MediaType} is returned with the type/subtype and all * parameters of this {@code ContentType}. */ Optional<MediaType> asMediaType(); } static final CharMatcher TSPECIALS = CharMatcher.anyOf("()<>@,;:\\\"/[]?="); static final CharMatcher TOKEN = CharMatcher.ASCII.and(CharMatcher.JAVA_ISO_CONTROL.negate()) .and(CharMatcher.isNot(' ')).and(TSPECIALS.negate()); /** * <i>very</i> lenient Content-Type or media type parsing. * <p> * <a href="http://tools.ietf.org/html/rfc822#section-3.3">RFC 822</a> * <a href="http://tools.ietf.org/html/rfc2045#section-5.1">RFC 2045</a> * <a href="http://tools.ietf.org/html/rfc2046">RFC 2046</a> */ static class LenientContentType implements ContentType { final CharSequence source; int pos; final int len; String type; String subtype; Multimap<String, String> params; ImmutableListMultimap<String, String> parameters; CAWriter buffer; boolean immediateParam; public LenientContentType(CharSequence input) { this.source = checkNotNull(input); this.len = input.length(); } private CAWriter getBuffer() { if (buffer == null) { buffer = new CAWriter(); } else { buffer.reset(); } return buffer; } void parse() { if (pos >= len) { return; } if (tryParseContentType() || tryParseFieldName()) { parseTypes(); } if (immediateParam) { parseParameter(); } while (pos < len) { if (source.charAt(pos++) == ';') { parseParameter(); } } buffer = null; } /** * attempts to parse a single parameter in form attribute=value */ void parseParameter() { parseDelimiter(); int start = pos, i = start; for (; i < len && TOKEN.matches(source.charAt(i)); i++) { /*parse attr*/} //attr may be empty String String attr = source.subSequence(start, i).toString(); pos = i; parseDelimiter(); if (pos >= len || source.charAt(pos) != '=') { //no =, only add value if there is an attr name if (!attr.isEmpty()) { addParam(attr, ""); } return; } pos++; parseDelimiter(); if (pos >= len) { addParam(attr, ""); return; } String value; if (source.charAt(pos) == '"') { value = parseString(); } else { start = i = pos; for (; i < len && TOKEN.matches(source.charAt(i)); i++) { /*parse value*/} //value may be empty String value = source.subSequence(start, i).toString(); pos = i; } addParam(attr, value); } private void addParam(String attr, String value) { if (params == null) { params = ArrayListMultimap.create(); } attr = normalizeToken(attr); params.put(attr, normalizeParameterValue(attr, checkNotNull(value))); } /** * parses a quoted String defined by quoted-string in RFC 822. unquoted * "CRLF LWSP-char" is stripped, quoted "CRLF LWSP-char" writes * CRLF and strips the single LWSP-char. does not require CR to be * escaped. */ String parseString() { assert source.charAt(pos) == '"'; pos++; CAWriter buf = getBuffer(); while (pos < len) { boolean append = true; char c = source.charAt(pos++); if (c == '"') { return buf.toString(); } else if (c == '\\') { if (pos < len) { c = source.charAt(pos++); if (c == '\r' && pos < len && source.charAt(pos) == '\n') { int p = pos + 1; if (p < len) { char lwspc = source.charAt(p); if (lwspc == ' ' || lwspc == '\t') { buf.write('\r'); buf.write('\n'); append = false; pos = p + 1; } } } } } else if (c == '\r') { if (pos < len && source.charAt(pos) == '\n') { int p = pos + 1; if (p < len) { char lwspc = source.charAt(p); if (lwspc == ' ' || lwspc == '\t') { append = false; pos = p + 1; } } } } if (append) { buf.append(c); } } return buf.toString(); } /** * attempts to parse the type/subtype */ void parseTypes() { if (pos >= len) { return; } parseDelimiter(); if (pos >= len) { return; } int start = pos, i = start; for (; i < len && TOKEN.matches(source.charAt(i)); i++) { /*parse type*/} if (i > start) { type = normalizeToken(source.subSequence(start, i).toString()); pos = i; } parseDelimiter(); if (pos >= len || source.charAt(pos) != '/') { return; } pos++; parseDelimiter(); start = i = pos; for (; i < len && TOKEN.matches(source.charAt(i)); i++) { /*parse subtype*/} if (i > start) { subtype = normalizeToken(source.subSequence(start, i).toString()); pos = i; } } static final char[] CT = "content-type".toCharArray(); /** * attempts to parse the leading "Content-Type:" string */ boolean tryParseContentType() { if (pos >= len) { return false; } parseDelimiter(); char c = source.charAt(pos); if (c != 'C' && c != 'c') { return false; } if (len - pos < CT.length) { return false; } for (int i = pos + 1, k = 1; k < CT.length; i++, k++) { if (Character.toLowerCase(source.charAt(i)) != CT[k]) { return false; } } pos += CT.length; if (pos < len && source.charAt(pos) == ':') { pos++; return true; } parseDelimiter(); if (pos < len && source.charAt(pos) == ':') { pos++; } return true; } /** * attempts to parse any field name * * @return true if the type/subtype should be parsed, false to skip * this and go straight to parsing parameters */ boolean tryParseFieldName() { int lastToken = -1; //start index of the last token encountered boolean inToken = false, //true when currently inside a token parseTypes = true, //true if type/subtype should be parsed broken = false; //true if the for loop was broken out of for (int p = pos; p < len; /*p incremented in loop*/) { char c = source.charAt(p); if (TOKEN.matches(c)) { if (!inToken) { inToken = true; lastToken = p; } p++; } else if (c == ':') { broken = true; pos = p + 1; break; } else if (c == '/' || c == ';') { broken = true; if (lastToken >= 0) { pos = lastToken; } else { pos = p; //can't increment as c could be ';' parseTypes = false; } break; } else if (c == '=') { broken = true; parseTypes = false; immediateParam = true; if (lastToken >= 0) { pos = lastToken; } break; } else { inToken = false; pos = p; parseDelimiter(); p = pos == p ? p + 1 : pos; } } if (!broken && lastToken >= 0) { pos = lastToken; } return parseTypes; } /** * skips white-space and comments */ void parseDelimiter() { while (pos < len) { char c = source.charAt(pos); if (CharMatcher.WHITESPACE.matches(c)) { pos++; } else if (c == '(') { parseComment(); } else { break; } } } /** * parses a single comment, the current char must be '(' * * @return true if the comment ended correctly */ boolean parseComment() { assert source.charAt(pos) == '('; pos++; int level = 1; while (pos < len) { char c = source.charAt(pos++); if (c == ')') { if (--level == 0) { return true; } } else if (c == '(') { level++; } else if (c == '\\') { pos++; } } return false; } @Override public CharSequence source() { return source; } @Override public Optional<String> type() { return Optional.fromNullable(type); } @Override public Optional<String> subtype() { return Optional.fromNullable(subtype); } @Override public ImmutableListMultimap<String, String> parameters() { if (parameters == null) { if (params != null) { parameters = ImmutableListMultimap.copyOf(params); params = null; } else { parameters = ImmutableListMultimap.of(); } } return parameters; } @Override public Optional<MediaType> asMediaType() { if (type != null && subtype != null) { MediaType mt = MediaType.create(type, subtype); return Optional.of(parameters().isEmpty() ? mt : mt.withParameters(parameters())); } else { return Optional.absent(); } } MediaType convert() { if (type != null && subtype != null) { MediaType mt = MediaType.create(type, subtype); return parameters().isEmpty() ? mt : mt.withParameters(parameters()); } else { throw new IllegalStateException(String.format("missing type/subtype; %s", this)); } } @Override public String toString() { return MoreObjects.toStringHelper(this).add("source", source).add("type", type).add("subtype", subtype) .add("parameters", parameters()).toString(); } } //normalizeToken and normalizeParameterValue are the same as guava MediaType static String normalizeToken(String token) { return Ascii.toLowerCase(token); } static String normalizeParameterValue(String attribute, String value) { return "charset".equals(attribute) ? Ascii.toLowerCase(value) : value; } private static final ImmutableSet<String> HTTP_HEADERS = ImmutableSet.of(HttpHeaders.CACHE_CONTROL, HttpHeaders.CONTENT_LENGTH, HttpHeaders.CONTENT_TYPE, HttpHeaders.DATE, HttpHeaders.PRAGMA, HttpHeaders.VIA, HttpHeaders.WARNING, HttpHeaders.ACCEPT, HttpHeaders.ACCEPT_CHARSET, HttpHeaders.ACCEPT_ENCODING, HttpHeaders.ACCEPT_LANGUAGE, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.AUTHORIZATION, HttpHeaders.CONNECTION, HttpHeaders.COOKIE, HttpHeaders.EXPECT, HttpHeaders.FROM, HttpHeaders.FOLLOW_ONLY_WHEN_PRERENDER_SHOWN, HttpHeaders.HOST, HttpHeaders.IF_MATCH, HttpHeaders.IF_MODIFIED_SINCE, HttpHeaders.IF_NONE_MATCH, HttpHeaders.IF_RANGE, HttpHeaders.IF_UNMODIFIED_SINCE, HttpHeaders.LAST_EVENT_ID, HttpHeaders.MAX_FORWARDS, HttpHeaders.ORIGIN, HttpHeaders.PROXY_AUTHORIZATION, HttpHeaders.RANGE, HttpHeaders.REFERER, HttpHeaders.TE, HttpHeaders.UPGRADE, HttpHeaders.USER_AGENT, HttpHeaders.ACCEPT_RANGES, HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.ACCESS_CONTROL_MAX_AGE, HttpHeaders.AGE, HttpHeaders.ALLOW, HttpHeaders.CONTENT_DISPOSITION, HttpHeaders.CONTENT_ENCODING, HttpHeaders.CONTENT_LANGUAGE, HttpHeaders.CONTENT_LOCATION, HttpHeaders.CONTENT_MD5, HttpHeaders.CONTENT_RANGE, HttpHeaders.CONTENT_SECURITY_POLICY, HttpHeaders.CONTENT_SECURITY_POLICY_REPORT_ONLY, HttpHeaders.ETAG, HttpHeaders.EXPIRES, HttpHeaders.LAST_MODIFIED, HttpHeaders.LINK, HttpHeaders.LOCATION, HttpHeaders.P3P, HttpHeaders.PROXY_AUTHENTICATE, HttpHeaders.REFRESH, HttpHeaders.RETRY_AFTER, HttpHeaders.SERVER, HttpHeaders.SET_COOKIE, HttpHeaders.SET_COOKIE2, HttpHeaders.STRICT_TRANSPORT_SECURITY, HttpHeaders.TIMING_ALLOW_ORIGIN, HttpHeaders.TRAILER, HttpHeaders.TRANSFER_ENCODING, HttpHeaders.VARY, HttpHeaders.WWW_AUTHENTICATE, HttpHeaders.DNT, HttpHeaders.X_CONTENT_TYPE_OPTIONS, HttpHeaders.X_DO_NOT_TRACK, HttpHeaders.X_FORWARDED_FOR, HttpHeaders.X_FORWARDED_PROTO, HttpHeaders.X_FRAME_OPTIONS, HttpHeaders.X_POWERED_BY, HttpHeaders.PUBLIC_KEY_PINS, HttpHeaders.PUBLIC_KEY_PINS_REPORT_ONLY, HttpHeaders.X_REQUESTED_WITH, HttpHeaders.X_USER_IP, HttpHeaders.X_XSS_PROTECTION); private static String canonicalize(String name) { name = name.trim(); while (name.endsWith(":")) { name = name.substring(0, name.length() - 1).trim(); } return Ascii.toLowerCase(name); } private static final Map<String, String> HEADER_MAP = Maps.newHashMap(); static { for (String name : HTTP_HEADERS) { HEADER_MAP.put(canonicalize(name), name); } } /** * Returns all headers found in {@link HttpHeaders} as an * {@code ImmutableSet}. */ public static ImmutableSet<String> httpHeaders() { return HTTP_HEADERS; } /** * Normalizes the header name to standard name found in {@link HttpHeaders}. * If {@code headerName} does not match any header in {@code HttpHeaders}, * it is returned as is. * * @param headerName the header name to normalize * @return the normalized header or null if {@code headerName} is null */ public static String normalize(@Nullable String headerName) { if (headerName == null || HTTP_HEADERS.contains(headerName)) { return headerName; } return MoreObjects.firstNonNull(HEADER_MAP.get(canonicalize(headerName)), headerName); } private static final Function<String, String> NORMALIZE = new Function<String, String>() { @Override public String apply(String input) { return normalize(input); } }; public static Function<String, String> normalize() { return NORMALIZE; } }