io.grpc.Metadata Maven / Gradle / Ivy
/*
* Copyright 2014, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc;
import static com.google.common.base.Charsets.US_ASCII;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Provides access to read and write metadata values to be exchanged during a call.
*
* This class is not thread safe, implementations should ensure that header reads and writes
* do not occur in multiple threads concurrently.
*
*/
@NotThreadSafe
public abstract class Metadata {
/**
* All binary headers should have this suffix in their names. Vice versa.
*/
public static final String BINARY_HEADER_SUFFIX = "-bin";
/**
* Simple metadata marshaller that encodes strings as is.
*
* This should be used with ASCII strings that only contain printable characters and space.
* Otherwise the output may be considered invalid and discarded by the transport.
*/
public static final AsciiMarshaller ASCII_STRING_MARSHALLER =
new AsciiMarshaller() {
@Override
public String toAsciiString(String value) {
return value;
}
@Override
public String parseAsciiString(String serialized) {
return serialized;
}
};
/**
* Simple metadata marshaller that encodes an integer as a signed decimal string.
*/
public static final AsciiMarshaller INTEGER_MARSHALLER = new AsciiMarshaller() {
@Override
public String toAsciiString(Integer value) {
return value.toString();
}
@Override
public Integer parseAsciiString(String serialized) {
return Integer.parseInt(serialized);
}
};
/** All value lists can be added to. No value list may be empty. */
// Use LinkedHashMap for consistent ordering for tests.
private final Map> store =
new LinkedHashMap>();
/**
* Constructor called by the transport layer when it receives binary metadata.
*/
// TODO(louiscryan): Convert to use ByteString so we can cache transformations
private Metadata(byte[]... binaryValues) {
for (int i = 0; i < binaryValues.length; i++) {
String name = new String(binaryValues[i], US_ASCII);
storeAdd(name, new MetadataEntry(name.endsWith(BINARY_HEADER_SUFFIX), binaryValues[++i]));
}
}
/**
* Constructor called by the application layer when it wants to send metadata.
*/
private Metadata() {}
private void storeAdd(String name, MetadataEntry value) {
List values = store.get(name);
if (values == null) {
values = new ArrayList();
store.put(name, values);
}
values.add(value);
}
/**
* Returns true if a value is defined for the given key.
*/
public boolean containsKey(Key key) {
return store.containsKey(key.name);
}
/**
* Returns the last metadata entry added with the name 'name' parsed as T.
* @return the parsed metadata entry or null if there are none.
*/
public T get(Key key) {
List values = store.get(key.name());
if (values == null) {
return null;
}
MetadataEntry metadataEntry = values.get(values.size() - 1);
return metadataEntry.getParsed(key);
}
/**
* Returns all the metadata entries named 'name', in the order they were received,
* parsed as T or null if there are none. The iterator is not guaranteed to be "live." It may or
* may not be accurate if Metadata is mutated.
*/
public Iterable getAll(final Key key) {
if (containsKey(key)) {
return Iterables.transform(
store.get(key.name()),
new Function() {
@Override
public T apply(MetadataEntry entry) {
return entry.getParsed(key);
}
});
}
return null;
}
/**
* Adds the {@code key, value} pair. If {@code key} already has values, {@code value} is added to
* the end. Duplicate values for the same key are permitted.
*
* @throws NullPointerException if key or value is null
*/
public void put(Key key, T value) {
Preconditions.checkNotNull(key, "key");
Preconditions.checkNotNull(value, "value");
storeAdd(key.name(), new MetadataEntry(key, value));
}
/**
* Removes the first occurence of value for key.
*
* @param key key for value
* @param value value
* @return {@code true} if {@code value} removed; {@code false} if {@code value} was not present
* @throws NullPointerException if {@code key} or {@code value} is null
*/
public boolean remove(Key key, T value) {
Preconditions.checkNotNull(key, "key");
Preconditions.checkNotNull(value, "value");
List values = store.get(key.name());
if (values == null) {
return false;
}
for (int i = 0; i < values.size(); i++) {
MetadataEntry entry = values.get(i);
if (!value.equals(entry.getParsed(key))) {
continue;
}
values.remove(i);
return true;
}
return false;
}
/**
* Remove all values for the given key. If there were no values, {@code null} is returned.
*/
public Iterable removeAll(final Key key) {
List values = store.remove(key.name());
if (values == null) {
return null;
}
return Iterables.transform(values, new Function() {
@Override
public T apply(MetadataEntry metadataEntry) {
return metadataEntry.getParsed(key);
}
});
}
/**
* Serialize all the metadata entries.
*
* It produces serialized names and values interleaved. result[i*2] are names, while
* result[i*2+1] are values.
*
*
Names are ASCII string bytes. If the name ends with "-bin", the value can be raw binary.
* Otherwise, the value must be printable ASCII characters or space.
*
*
The returned byte arrays must not be modified.
*
*
This method is intended for transport use only.
*/
public byte[][] serialize() {
// One *2 for keys+values, one *2 to prevent resizing if a single key has multiple values
List serialized = new ArrayList(store.size() * 2 * 2);
for (Map.Entry> keyEntry : store.entrySet()) {
for (int i = 0; i < keyEntry.getValue().size(); i++) {
MetadataEntry entry = keyEntry.getValue().get(i);
byte[] asciiName;
if (entry.key != null) {
asciiName = entry.key.asciiName();
} else {
asciiName = keyEntry.getKey().getBytes(US_ASCII);
}
serialized.add(asciiName);
serialized.add(entry.getSerialized());
}
}
return serialized.toArray(new byte[serialized.size()][]);
}
/**
* Perform a simple merge of two sets of metadata.
*/
public void merge(Metadata other) {
Preconditions.checkNotNull(other);
for (Map.Entry> keyEntry : other.store.entrySet()) {
for (int i = 0; i < keyEntry.getValue().size(); i++) {
// Must copy the MetadataEntries since they are mutated. If the two Metadata objects are
// used from different threads it would cause thread-safety issues.
storeAdd(keyEntry.getKey(), new MetadataEntry(keyEntry.getValue().get(i)));
}
}
}
/**
* Merge values for the given set of keys into this set of metadata.
*/
public void merge(Metadata other, Set> keys) {
Preconditions.checkNotNull(other);
for (Key key : keys) {
List values = other.store.get(key.name);
if (values == null) {
continue;
}
for (int i = 0; i < values.size(); i++) {
// Must copy the MetadataEntries since they are mutated. If the two Metadata objects are
// used from different threads it would cause thread-safety issues.
storeAdd(key.name, new MetadataEntry(values.get(i)));
}
}
}
private String toStringInternal() {
return store.toString();
}
/**
* Concrete instance for metadata attached to the start of a call.
*/
public static class Headers extends Metadata {
private String path;
private String authority;
/**
* Called by the transport layer to create headers from their binary serialized values.
*
* This method does not copy the provided byte arrays. The byte arrays must not be mutated.
*/
public Headers(byte[]... headers) {
super(headers);
}
/**
* Called by the application layer to construct headers prior to passing them to the
* transport for serialization.
*/
public Headers() {
}
/**
* The path for the operation.
*/
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
/**
* The serving authority for the operation.
*/
public String getAuthority() {
return authority;
}
/**
* Override the HTTP/2 authority the channel claims to be connecting to. This is not
* generally safe. Overriding allows advanced users to re-use a single Channel for multiple
* services, even if those services are hosted on different domain names. That assumes the
* server is virtually hosting multiple domains and is guaranteed to continue doing so. It is
* rare for a service provider to make such a guarantee. At this time, there is no security
* verification of the overridden value, such as making sure the authority matches the server's
* TLS certificate.
*/
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public void merge(Metadata other) {
super.merge(other);
mergePathAndAuthority(other);
}
@Override
public void merge(Metadata other, Set> keys) {
super.merge(other, keys);
mergePathAndAuthority(other);
}
private void mergePathAndAuthority(Metadata other) {
if (other instanceof Headers) {
Headers otherHeaders = (Headers) other;
path = otherHeaders.path != null ? otherHeaders.path : path;
authority = otherHeaders.authority != null ? otherHeaders.authority : authority;
}
}
@Override
public String toString() {
return "Headers(path=" + path
+ ",authority=" + authority
+ ",metadata=" + super.toStringInternal() + ")";
}
}
/**
* Concrete instance for metadata attached to the end of the call. Only provided by
* servers.
*/
public static class Trailers extends Metadata {
/**
* Called by the transport layer to create trailers from their binary serialized values.
*
* This method does not copy the provided byte arrays. The byte arrays must not be mutated.
*/
public Trailers(byte[]... headers) {
super(headers);
}
/**
* Called by the application layer to construct trailers prior to passing them to the
* transport for serialization.
*/
public Trailers() {
}
@Override
public String toString() {
return "Trailers(" + super.toStringInternal() + ")";
}
}
/**
* Marshaller for metadata values that are serialized into raw binary.
*/
public interface BinaryMarshaller {
/**
* Serialize a metadata value to bytes.
* @param value to serialize
* @return serialized version of value
*/
byte[] toBytes(T value);
/**
* Parse a serialized metadata value from bytes.
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
T parseBytes(byte[] serialized);
}
/**
* Marshaller for metadata values that are serialized into ASCII strings that contain only
* printable characters and space.
*/
public interface AsciiMarshaller {
/**
* Serialize a metadata value to a ASCII string that contains only printable characters and
* space.
*
* @param value to serialize
* @return serialized version of value, or null if value cannot be transmitted.
*/
String toAsciiString(T value);
/**
* Parse a serialized metadata value from an ASCII string.
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
T parseAsciiString(String serialized);
}
/**
* Key for metadata entries. Allows for parsing and serialization of metadata.
*/
public abstract static class Key {
/**
* Creates a key for a binary header.
*
* @param name must end with {@link #BINARY_HEADER_SUFFIX}
*/
public static Key of(String name, BinaryMarshaller marshaller) {
return new BinaryKey(name, marshaller);
}
/**
* Creates a key for a ASCII header.
*
* @param name must not end with {@link #BINARY_HEADER_SUFFIX}
*/
public static Key of(String name, AsciiMarshaller marshaller) {
return new AsciiKey(name, marshaller);
}
private final String name;
private final byte[] asciiName;
private Key(String name) {
this.name = Preconditions.checkNotNull(name, "name").toLowerCase(Locale.ROOT).intern();
this.asciiName = this.name.getBytes(US_ASCII);
}
public String name() {
return name;
}
/**
* Get the name as bytes using ASCII-encoding.
*
* The returned byte arrays must not be modified.
*
*
This method is intended for transport use only.
*/
// TODO (louiscryan): Migrate to ByteString
@VisibleForTesting
byte[] asciiName() {
return asciiName;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Key key = (Key) o;
return name.equals(key.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return "Key{name='" + name + "'}";
}
/**
* Serialize a metadata value to bytes.
* @param value to serialize
* @return serialized version of value
*/
abstract byte[] toBytes(T value);
/**
* Parse a serialized metadata value from bytes.
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
abstract T parseBytes(byte[] serialized);
}
private static class BinaryKey extends Key {
private final BinaryMarshaller marshaller;
/**
* Keys have a name and a binary marshaller used for serialization.
*/
private BinaryKey(String name, BinaryMarshaller marshaller) {
super(name);
Preconditions.checkArgument(name.endsWith(BINARY_HEADER_SUFFIX),
"Binary header is named " + name + ". It must end with " + BINARY_HEADER_SUFFIX);
this.marshaller = Preconditions.checkNotNull(marshaller);
}
@Override
byte[] toBytes(T value) {
return marshaller.toBytes(value);
}
@Override
T parseBytes(byte[] serialized) {
return marshaller.parseBytes(serialized);
}
}
private static class AsciiKey extends Key {
private final AsciiMarshaller marshaller;
/**
* Keys have a name and an ASCII marshaller used for serialization.
*/
private AsciiKey(String name, AsciiMarshaller marshaller) {
super(name);
Preconditions.checkArgument(!name.endsWith(BINARY_HEADER_SUFFIX),
"ASCII header is named " + name + ". It must not end with " + BINARY_HEADER_SUFFIX);
this.marshaller = Preconditions.checkNotNull(marshaller);
}
@Override
byte[] toBytes(T value) {
return marshaller.toAsciiString(value).getBytes(US_ASCII);
}
@Override
T parseBytes(byte[] serialized) {
return marshaller.parseAsciiString(new String(serialized, US_ASCII));
}
}
private static class MetadataEntry {
Object parsed;
@SuppressWarnings("rawtypes")
Key key;
boolean isBinary;
byte[] serializedBinary;
/**
* Constructor used when application layer adds a parsed value.
*/
private MetadataEntry(Key key, Object parsed) {
this.parsed = Preconditions.checkNotNull(parsed);
this.key = Preconditions.checkNotNull(key);
this.isBinary = key instanceof BinaryKey;
}
/**
* Constructor used when reading a value from the transport.
*/
private MetadataEntry(boolean isBinary, byte[] serialized) {
Preconditions.checkNotNull(serialized);
this.serializedBinary = serialized;
this.isBinary = isBinary;
}
/**
* Copy constructor.
*/
private MetadataEntry(MetadataEntry entry) {
this.parsed = entry.parsed;
this.key = entry.key;
this.isBinary = entry.isBinary;
this.serializedBinary = entry.serializedBinary;
}
@SuppressWarnings("unchecked")
public T getParsed(Key key) {
T value = (T) parsed;
if (value != null) {
if (this.key != key) {
// Keys don't match so serialize using the old key
serializedBinary = this.key.toBytes(value);
} else {
return value;
}
}
this.key = key;
if (serializedBinary != null) {
value = key.parseBytes(serializedBinary);
}
parsed = value;
return value;
}
@SuppressWarnings("unchecked")
public byte[] getSerialized() {
return serializedBinary =
serializedBinary == null
? key.toBytes(parsed) : serializedBinary;
}
@Override
public String toString() {
if (!isBinary) {
return new String(getSerialized(), US_ASCII);
} else {
// Assume that the toString of an Object is better than a binary encoding.
if (parsed != null) {
return "" + parsed;
} else {
return Arrays.toString(serializedBinary);
}
}
}
}
}