/*
* Copyright 2018, OpenCensus Authors
*
* 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 io.opencensus.trace;
import com.google.auto.value.AutoValue;
import io.opencensus.common.ExperimentalApi;
import io.opencensus.internal.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.concurrent.Immutable;
/**
* Carries tracing-system specific context in a list of key-value pairs. TraceState allows different
* vendors propagate additional information and inter-operate with their legacy Id formats.
*
*
Implementation is optimized for a small list of key-value pairs.
*
*
Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter,
* and can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
* forward slashes /.
*
*
Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the
* range 0x20 to 0x7E) except comma , and =.
*
* @since 0.16
*/
@Immutable
@AutoValue
@ExperimentalApi
public abstract class Tracestate {
private static final int KEY_MAX_SIZE = 256;
private static final int VALUE_MAX_SIZE = 256;
private static final int MAX_KEY_VALUE_PAIRS = 32;
/**
* Returns the value to which the specified key is mapped, or null if this map contains no mapping
* for the key.
*
* @param key with which the specified value is to be associated
* @return the value to which the specified key is mapped, or null if this map contains no mapping
* for the key.
* @since 0.16
*/
@javax.annotation.Nullable
public String get(String key) {
for (Entry entry : getEntries()) {
if (entry.getKey().equals(key)) {
return entry.getValue();
}
}
return null;
}
/**
* Returns a {@link List} view of the mappings contained in this {@code TraceState}.
*
* @return a {@link List} view of the mappings contained in this {@code TraceState}.
* @since 0.16
*/
public abstract List getEntries();
/**
* Returns a {@code Builder} based on an empty {@code Tracestate}.
*
* @return a {@code Builder} based on an empty {@code Tracestate}.
* @since 0.16
*/
public static Builder builder() {
return new Builder(Builder.EMPTY);
}
/**
* Returns a {@code Builder} based on this {@code Tracestate}.
*
* @return a {@code Builder} based on this {@code Tracestate}.
* @since 0.16
*/
public Builder toBuilder() {
return new Builder(this);
}
/**
* Builder class for {@link MessageEvent}.
*
* @since 0.16
*/
@ExperimentalApi
public static final class Builder {
private final Tracestate parent;
@javax.annotation.Nullable private ArrayList entries;
// Needs to be in this class to avoid initialization deadlock because super class depends on
// subclass (the auto-value generate class).
private static final Tracestate EMPTY = create(Collections.emptyList());
private Builder(Tracestate parent) {
Utils.checkNotNull(parent, "parent");
this.parent = parent;
this.entries = null;
}
/**
* Adds or updates the {@code Entry} that has the given {@code key} if it is present. The new
* {@code Entry} will always be added in the front of the list of entries.
*
* @param key the key for the {@code Entry} to be added.
* @param value the value for the {@code Entry} to be added.
* @return this.
* @since 0.16
*/
@SuppressWarnings("nullness")
public Builder set(String key, String value) {
// Initially create the Entry to validate input.
Entry entry = Entry.create(key, value);
if (entries == null) {
// Copy entries from the parent.
entries = new ArrayList(parent.getEntries());
}
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i).getKey().equals(entry.getKey())) {
entries.remove(i);
// Exit now because the entries list cannot contain duplicates.
break;
}
}
// Inserts the element at the front of this list.
entries.add(0, entry);
return this;
}
/**
* Removes the {@code Entry} that has the given {@code key} if it is present.
*
* @param key the key for the {@code Entry} to be removed.
* @return this.
* @since 0.16
*/
@SuppressWarnings("nullness")
public Builder remove(String key) {
Utils.checkNotNull(key, "key");
if (entries == null) {
// Copy entries from the parent.
entries = new ArrayList(parent.getEntries());
}
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i).getKey().equals(key)) {
entries.remove(i);
// Exit now because the entries list cannot contain duplicates.
break;
}
}
return this;
}
/**
* Builds a TraceState by adding the entries to the parent in front of the key-value pairs list
* and removing duplicate entries.
*
* @return a TraceState with the new entries.
* @since 0.16
*/
public Tracestate build() {
if (entries == null) {
return parent;
}
return Tracestate.create(entries);
}
}
/**
* Immutable key-value pair for {@code Tracestate}.
*
* @since 0.16
*/
@Immutable
@AutoValue
@ExperimentalApi
public abstract static class Entry {
/**
* Creates a new {@code Entry} for the {@code Tracestate}.
*
* @param key the Entry's key.
* @param value the Entry's value.
* @since 0.16
*/
public static Entry create(String key, String value) {
Utils.checkNotNull(key, "key");
Utils.checkNotNull(value, "value");
Utils.checkArgument(validateKey(key), "Invalid key %s", key);
Utils.checkArgument(validateValue(value), "Invalid value %s", value);
return new AutoValue_Tracestate_Entry(key, value);
}
/**
* Returns the key {@code String}.
*
* @return the key {@code String}.
* @since 0.16
*/
public abstract String getKey();
/**
* Returns the value {@code String}.
*
* @return the value {@code String}.
* @since 0.16
*/
public abstract String getValue();
Entry() {}
}
// Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter, and
// can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
// forward slashes /.
private static boolean validateKey(String key) {
if (key.length() > KEY_MAX_SIZE
|| key.isEmpty()
|| key.charAt(0) < 'a'
|| key.charAt(0) > 'z') {
return false;
}
for (int i = 1; i < key.length(); i++) {
char c = key.charAt(i);
if (!(c >= 'a' && c <= 'z')
&& !(c >= '0' && c <= '9')
&& c != '_'
&& c != '-'
&& c != '*'
&& c != '/') {
return false;
}
}
return true;
}
// Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the range
// 0x20 to 0x7E) except comma , and =.
private static boolean validateValue(String value) {
if (value.length() > VALUE_MAX_SIZE || value.charAt(value.length() - 1) == ' ' /* '\u0020' */) {
return false;
}
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c == ',' || c == '=' || c < ' ' /* '\u0020' */ || c > '~' /* '\u007E' */) {
return false;
}
}
return true;
}
private static Tracestate create(List entries) {
Utils.checkState(entries.size() <= MAX_KEY_VALUE_PAIRS, "Invalid size");
return new AutoValue_Tracestate(Collections.unmodifiableList(entries));
}
Tracestate() {}
}