diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index b67e335b8d..5aef217f67 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -18,7 +18,7 @@ can be automatically added by running `./mvnw com.mycila:license-maven-plugin:fo
```
/**
- * Copyright 2019 The OpenZipkin Authors
+ * Copyright 2020 The OpenZipkin 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
diff --git a/HACKING.md b/HACKING.md
index 27cef78ead..69ddb1252c 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -27,6 +27,9 @@ the intent of the feature are often merged and released in days. If a
merged change isn't immediately released and it is of priority to you,
nag (make a comment) on your merged pull request until it is released.
+It is best to review rationale documents such as [brave/RATIONALE.md]
+prior to raising a pull request.
+
## How to experiment
Changes to Brave's code are best addressed by the feature requestor in a
pull request *after* discussing in an issue or on gitter. By discussing
diff --git a/brave-tests/src/main/java/brave/test/propagation/PropagationTest.java b/brave-tests/src/main/java/brave/test/propagation/PropagationTest.java
index f36de3b11c..ee51793343 100644
--- a/brave-tests/src/main/java/brave/test/propagation/PropagationTest.java
+++ b/brave-tests/src/main/java/brave/test/propagation/PropagationTest.java
@@ -13,7 +13,7 @@
*/
package brave.test.propagation;
-import brave.internal.HexCodec;
+import brave.internal.codec.HexCodec;
import brave.internal.Nullable;
import brave.propagation.Propagation;
import brave.propagation.Propagation.Getter;
diff --git a/brave/RATIONALE.md b/brave/RATIONALE.md
index 8530a9b311..7d3c03df8a 100644
--- a/brave/RATIONALE.md
+++ b/brave/RATIONALE.md
@@ -18,10 +18,57 @@ thought as it would betray productivity and make this document unreadable.
Rationale here should be limited to impactful designs, and aspects non-obvious,
non-conventional or subtle.
-## Public namespace
+## Java conventions
Brave 4's public namespace is more defensive that the past, using a package
accessor design from [OkHttp](https://github.com/square/okhttp).
+We only expose types public internally or after significant demand. This keeps
+the api small and easier to manage when charting migration paths. Otherwise,
+types are always package private.
+
+Methods should only be marked public when they are intentional apis or
+inheritance requires it. This practice prevents accidental dependence on
+utilities.
+
+### Why no private symbols? (methods and fields)
+Brave is a library with embedded use cases, such as inside Java agents or
+Android code.
+
+For example, Android has a []hard limit on total methods in an application](https://developer.android.com/studio/build/multidex#avoid).
+Fields marked private imply accessors in order to share state in the same
+package. We routinely share state, such as sampling flag internals inside a
+package. If we marked fields private, we'd count against that limit without
+adding value.
+
+Modifiers on fields and methods are distracting to read and increase the size
+of the bytecode generated during compilation. By recovering the size otherwise
+spent on private modifiers, we not only avoid hitting limits, but we are also
+able to add more code with the same jar size.
+
+For example, Brave 5.12 remains less than 250KiB, with no dependencies, all
+features including deprecation bridges, and an embedded json serializer.
+
+This means we do not support sharing our packages with third parties, but we
+do support an "always share inside a package" in our repository. In other
+words, we trust our developers to proceed with caution. In the first seven
+years of project history, we have had no issues raised with this policy.
+
+### Zero dependency policy
+Brave is a telemetry library, which means it is used inside other low-level
+libraries. We are unable to predict the version ranges of those libraries, and
+attempting to do that would limit the applicability of Brave, which is an anti-
+goal. Instead, we choose to use nothing except floor Java version features,
+currently Java 6.
+
+### Why `new NullPointerException("xxx == null")`
+For public entry points, we eagerly check null and throw `NullPointerException`
+with a message like "xxx == null". This is not a normal pre-condition, such as
+argument validation, which you'd throw `IllegalArgumentException` for. What's
+happening here is we are making debugging (literally NPEs are bugs) easier, by
+not deferring to Java to raise the NPE. If we deferred, it could be confusing
+which local was null, especially as deferring results in an exception with no
+message.
+
## Stateful Span object
Brave 4 allows you to pass around a Span object which can report itself
to Zipkin when finished. This is better than using thread contexts in
diff --git a/brave/src/main/java/brave/Tracing.java b/brave/src/main/java/brave/Tracing.java
index f6b023553b..285299dcaa 100644
--- a/brave/src/main/java/brave/Tracing.java
+++ b/brave/src/main/java/brave/Tracing.java
@@ -16,7 +16,7 @@
import brave.baggage.BaggageField;
import brave.handler.FinishedSpanHandler;
import brave.handler.MutableSpan;
-import brave.internal.IpLiteral;
+import brave.internal.codec.IpLiteral;
import brave.internal.Nullable;
import brave.internal.Platform;
import brave.internal.handler.NoopAwareFinishedSpanHandler;
diff --git a/brave/src/main/java/brave/handler/MutableSpan.java b/brave/src/main/java/brave/handler/MutableSpan.java
index 14265dedeb..2f44d00698 100644
--- a/brave/src/main/java/brave/handler/MutableSpan.java
+++ b/brave/src/main/java/brave/handler/MutableSpan.java
@@ -16,7 +16,7 @@
import brave.ErrorParser;
import brave.Span.Kind;
import brave.SpanCustomizer;
-import brave.internal.IpLiteral;
+import brave.internal.codec.IpLiteral;
import brave.internal.Nullable;
import brave.propagation.TraceContext;
import java.lang.ref.WeakReference;
@@ -24,7 +24,7 @@
import static brave.internal.InternalPropagation.FLAG_DEBUG;
import static brave.internal.InternalPropagation.FLAG_SHARED;
-import static brave.internal.JsonEscaper.jsonEscape;
+import static brave.internal.codec.JsonEscaper.jsonEscape;
/**
* This represents a span except for its {@link TraceContext}. It is mutable, for late adjustments.
diff --git a/brave/src/main/java/brave/internal/codec/EntrySplitter.java b/brave/src/main/java/brave/internal/codec/EntrySplitter.java
new file mode 100644
index 0000000000..cb584ef661
--- /dev/null
+++ b/brave/src/main/java/brave/internal/codec/EntrySplitter.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2013-2020 The OpenZipkin 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 brave.internal.codec;
+
+import brave.internal.Platform;
+
+/**
+ * Splits a character sequence that's in a delimited string, optionally trimming optional whitespace
+ * (OWS) before or after
+ * delimiters.
+ *
+ *
This is intended to be initialized as a constant, as doing so per-request will add
+ * unnecessary overhead.
+ */
+public final class EntrySplitter {
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ int maxEntries = Integer.MAX_VALUE;
+ char entrySeparator = ',', keyValueSeparator = '=';
+ boolean trimOWSAroundEntrySeparator = true, trimOWSAroundKeyValueSeparator = true;
+ boolean keyValueSeparatorRequired = true, shouldThrow = false;
+
+ /**
+ * When set, {@link Handler} will be called maximum {@code maxEntries} times per parse. After
+ * that, {@link #parse(Handler, Object, String)} returns false or throws an exception, based on
+ * {@link #shouldThrow(boolean)}. Default: {@link Integer#MAX_VALUE}.
+ *
+ *
This is used to implement strict format constraints. For example, above 32 entries is
+ * malformed. This is separate from any capacity constraints of the {@link Handler}, which may
+ * be smaller than this amount.
+ */
+ public Builder maxEntries(int maxEntries) {
+ if (maxEntries <= 0) throw new IllegalArgumentException("maxEntries <= 0");
+ this.maxEntries = maxEntries;
+ return this;
+ }
+
+ /**
+ * The separator to use between entries. For example, the input "k1=v1,k2=v2", should have
+ * {@code entrySeparator} ','. Default: ','
+ *
+ * @see #keyValueSeparator(char)
+ */
+ public Builder entrySeparator(char entrySeparator) {
+ if (entrySeparator == 0) throw new IllegalArgumentException("entrySeparator == 0");
+ this.entrySeparator = entrySeparator;
+ return this;
+ }
+
+ /**
+ * The separator to use between a key and value. For example, the input "k1=v1,k2=v2" should
+ * have {@code keyValueSeparator} '='. Default: '='
+ *
+ *
Note: Only the first {@code keyValueSeparator} identifies the end of the key
+ * until the next {@link #entrySeparator(char)}. This means values can include the {@code
+ * keyValueSeparator} character.
+ *
+ *
For example, the input "condition=animal=cat" with {@code keyValueSeparator} '=' parses
+ * {@code [("condition", "animal=cat")]}
+ *
+ * @see #keyValueSeparator(char)
+ */
+ public Builder keyValueSeparator(char keyValueSeparator) {
+ if (keyValueSeparator == 0) throw new IllegalArgumentException("keyValueSeparator == 0");
+ this.keyValueSeparator = keyValueSeparator;
+ return this;
+ }
+
+ /**
+ * When {@code true}, optional whitespace (spaces and tabs aka OWS)
+ * are removed around the {@link #entrySeparator} and string boundaries. Default: {@code true}
+ *
+ *
For example, given the input " k1 = v1 , k2 = v2 ", this trims around the
+ * "=" character and string boundaries: {@code [("k1 "," v1"),("k2 ", " v2")]}.
+ *
+ * @see #trimOWSAroundKeyValueSeparator(boolean)
+ */
+ public Builder trimOWSAroundEntrySeparator(boolean trimOWSAroundEntrySeparator) {
+ this.trimOWSAroundEntrySeparator = trimOWSAroundEntrySeparator;
+ return this;
+ }
+
+ /**
+ * When {@code true}, optional whitespace (spaces and tabs aka OWS)
+ * are removed around the {@link #keyValueSeparator(char)}. Default: {@code true}
+ *
+ *
For example, given the input " k1 = v1 , k2 = v2 ", this trims around the
+ * "=" character and string boundaries: {@code [(" k1", "v1 "),(" k2", "v2 ")]}.
+ *
+ * @see #trimOWSAroundKeyValueSeparator(boolean)
+ */
+ public Builder trimOWSAroundKeyValueSeparator(boolean trimOWSAroundKeyValueSeparator) {
+ this.trimOWSAroundKeyValueSeparator = trimOWSAroundKeyValueSeparator;
+ return this;
+ }
+
+ /**
+ * When {@code true}, when a {@link #keyValueSeparator(char)} does not follow a key, {@link
+ * #parse(Handler, Object, String)} returns false or throws an exception, based on {@link
+ * #shouldThrow(boolean)}. Default: {@code true}.
+ *
+ *
Setting this to {@code false} makes "k1,k2=v2" interpreted the same as if there was
+ * a {@link #keyValueSeparator(char)}: "k1=,k2=v2". This is used for formats such as HTTP
+ * queries where separators are optional.
+ */
+ public Builder keyValueSeparatorRequired(boolean keyValueSeparatorRequired) {
+ this.keyValueSeparatorRequired = keyValueSeparatorRequired;
+ return this;
+ }
+
+ /**
+ * On validation fail, should this throw an exception or log?. The use case to throw is when
+ * validating input (ex into a builder), or in unit tests.
+ */
+ public Builder shouldThrow(boolean shouldThrow) {
+ this.shouldThrow = shouldThrow;
+ return this;
+ }
+
+ public EntrySplitter build() {
+ if (entrySeparator == keyValueSeparator) {
+ throw new IllegalArgumentException("entrySeparator == keyValueSeparator");
+ }
+ return new EntrySplitter(this);
+ }
+ }
+
+ /**
+ * This is a callback on offsets to avoid allocating strings for a malformed input {@code input}.
+ *
+ * @param target of parsed entries
+ */
+ public interface Handler {
+ /**
+ * Called for each valid entry split from the input {@code input}. Return {@code false} after
+ * logging to stop due to invalid input.
+ *
+ * After validating, typically strings will be parsed from the input like so:
+ *
{@code
+ * String key = input.substring(beginKey, endKey);
+ * String value = input.substring(beginValue, endValue);
+ * }
+ *
+ * @param target receiver of parsed entries
+ * @param input string including data to parse
+ * @param beginKey begin index of the entry's key in {@code input}, inclusive
+ * @param endKey end index of the entry's key in {@code input}, exclusive
+ * @param beginValue begin index of the entry's value in {@code input}, inclusive
+ * @param endValue end index of the entry's value in {@code input}, exclusive
+ * @return true if we reached the {@code endIndex} without failures.
+ */
+ boolean onEntry(
+ T target, String input, int beginKey, int endKey, int beginValue, int endValue);
+ }
+
+ final char keyValueSeparator, entrySeparator;
+ int maxEntries;
+ final boolean trimOWSAroundEntrySeparator, trimOWSAroundKeyValueSeparator;
+ final boolean keyValueSeparatorRequired, shouldThrow;
+ final String missingKey, missingKeyValueSeparator, overMaxEntries;
+
+ EntrySplitter(Builder builder) {
+ keyValueSeparator = builder.keyValueSeparator;
+ entrySeparator = builder.entrySeparator;
+ maxEntries = builder.maxEntries;
+ trimOWSAroundEntrySeparator = builder.trimOWSAroundEntrySeparator;
+ trimOWSAroundKeyValueSeparator = builder.trimOWSAroundKeyValueSeparator;
+ keyValueSeparatorRequired = builder.keyValueSeparatorRequired;
+ shouldThrow = builder.shouldThrow;
+ missingKey = "Invalid input: no key before '" + keyValueSeparator + "'";
+ missingKeyValueSeparator =
+ "Invalid input: missing key value separator '" + keyValueSeparator + "'";
+ overMaxEntries = "Invalid input: over " + maxEntries + " entries";
+ }
+
+ /**
+ * @param handler parses entries emitted upon success
+ * @param target receiver of parsed entries
+ * @param input string including data to parse
+ * @return true if we reached the {@code endIndex} without failures.
+ */
+ public boolean parse(Handler handler, T target, String input) {
+ if (input == null) throw new NullPointerException("input == null");
+ return parse(handler, target, input, 0, input.length());
+ }
+
+ /**
+ * @param handler parses entries emitted upon success
+ * @param target receiver of parsed entries
+ * @param input string including data to parse
+ * @param beginIndex begin index of the {@code input}, inclusive
+ * @param endIndex end index of the {@code input}, exclusive
+ * @return true if we reached the {@code endIndex} without failures.
+ */
+ public boolean parse(
+ Handler handler, T target, String input, int beginIndex, int endIndex) {
+ if (handler == null) throw new NullPointerException("handler == null");
+ if (target == null) throw new NullPointerException("target == null");
+ if (input == null) throw new NullPointerException("input == null");
+ if (beginIndex < 0) throw new IllegalArgumentException("beginIndex < 0");
+ if (endIndex > input.length()) throw new IllegalArgumentException("endIndex > input.length()");
+
+ int remainingEntries = maxEntries, beginKey = -1, endKey = -1, beginValue = -1;
+ for (int i = beginIndex; i < endIndex; i++) {
+ char c = input.charAt(i);
+
+ boolean nextIsEnd = i + 1 == endIndex;
+ if (c == entrySeparator || nextIsEnd) { // finished an entry
+ if (c == keyValueSeparator) {
+ beginValue = i; // key separator at end of the input. ex "key=" or "k1 =", but not "k1"
+ }
+
+ if (beginKey == -1 && beginValue == -1) {
+ continue; // ignore empty entries, like ",,"
+ } else if (beginKey == -1) {
+ return logOrThrow(missingKey, shouldThrow); // ex. "=" ",="
+ } else if (nextIsEnd && beginValue == -1) {
+ // We reached the end of a key-only entry, a single character entry or an empty entry
+ // at the end of the input. ex "k1" "k1 " "a=b" "..=,"
+ beginValue = c == entrySeparator ? i + 1 : i;
+ }
+
+ int endValue;
+ if (endKey == -1) {
+ if (keyValueSeparatorRequired && c != keyValueSeparator) {
+ return logOrThrow(missingKeyValueSeparator, shouldThrow); // throw on "k1" "k1=v2,k2"
+ }
+
+ // Even though we have an empty value, we need to handle whitespace and
+ // boundary conditions.
+ //
+ // For example, using entry separator ',' and KV separator '=':
+ // "...,k1" and input[i] == 'y', we want i + 1, so that the key includes the 'y'
+ // "...,k1 " and input[i] == ' ', we want i + 1, as the key includes a trailing ' '
+ // "...,k1=" and input[i] == '=', we want i, bc a KV separator isn't part of the key
+ // "k1 , k2" and input[i] == ',', we want i, bc an entry separator isn't part of the key
+ endKey = nextIsEnd && c != keyValueSeparator ? i + 1 : i;
+
+ if (trimOWSAroundKeyValueSeparator) {
+ endKey = rewindOWS(input, beginKey, endKey);
+ }
+ beginValue = endValue = endKey; // value is empty
+ } else {
+ endValue = nextIsEnd ? i + 1 : i;
+
+ if (trimOWSAroundEntrySeparator) {
+ endValue = rewindOWS(input, beginValue, endValue);
+ }
+ }
+
+ if (remainingEntries-- == 0) logOrThrow(overMaxEntries, shouldThrow);
+
+ if (!handler.onEntry(target, input, beginKey, endKey, beginValue, endValue)) {
+ return false; // assume handler logs
+ }
+
+ beginKey = endKey = beginValue = -1; // reset for the next entry
+ } else if (beginKey == -1) {
+ if (trimOWSAroundEntrySeparator && isOWS(c)) continue; // skip whitespace before key
+ if (c == keyValueSeparator) {
+ if (i == beginIndex || input.charAt(i - 1) == entrySeparator) {
+ return logOrThrow(missingKey, shouldThrow); // ex "=v1" ",=v2"
+ }
+ }
+ beginKey = i;
+ } else if (endKey == -1 && c == keyValueSeparator) { // only use the first separator for key
+ endKey = i;
+ if (trimOWSAroundKeyValueSeparator) {
+ endKey = rewindOWS(input, beginIndex, endKey);
+ }
+ } else if (endKey != -1 && beginValue == -1) { // only look for a value if you have a key
+ if (trimOWSAroundKeyValueSeparator && isOWS(c)) continue; // skip whitespace before value
+ if (c == keyValueSeparator) continue; // skip the keyValueSeparator (ex '=')
+ beginValue = i;
+ }
+ }
+ return true;
+ }
+
+ static int rewindOWS(String input, int beginIndex, int endIndex) {
+ // endIndex is a boundary. we need to begin looking one character before it.
+ while (isOWS(input.charAt(endIndex - 1))) {
+ if (--endIndex == beginIndex) return beginIndex; // trim whitespace
+ }
+ return endIndex;
+ }
+
+ // OWS is zero or more spaces or tabs https://httpwg.org/specs/rfc7230.html#rfc.section.3.2
+ static boolean isOWS(char c) {
+ return c == ' ' || c == '\t';
+ }
+
+ static boolean logOrThrow(String msg, boolean shouldThrow) {
+ if (shouldThrow) throw new IllegalArgumentException(msg);
+ Platform.get().log(msg, null);
+ return false;
+ }
+}
diff --git a/brave/src/main/java/brave/internal/HexCodec.java b/brave/src/main/java/brave/internal/codec/HexCodec.java
similarity index 98%
rename from brave/src/main/java/brave/internal/HexCodec.java
rename to brave/src/main/java/brave/internal/codec/HexCodec.java
index 26706c6947..1db67d2c31 100644
--- a/brave/src/main/java/brave/internal/HexCodec.java
+++ b/brave/src/main/java/brave/internal/codec/HexCodec.java
@@ -11,7 +11,9 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
-package brave.internal;
+package brave.internal.codec;
+
+import brave.internal.RecyclableBuffers;
// code originally imported from zipkin.Util
public final class HexCodec {
diff --git a/brave/src/main/java/brave/internal/IpLiteral.java b/brave/src/main/java/brave/internal/codec/IpLiteral.java
similarity index 98%
rename from brave/src/main/java/brave/internal/IpLiteral.java
rename to brave/src/main/java/brave/internal/codec/IpLiteral.java
index 073bf654cd..222d7fa74e 100644
--- a/brave/src/main/java/brave/internal/IpLiteral.java
+++ b/brave/src/main/java/brave/internal/codec/IpLiteral.java
@@ -11,7 +11,9 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
-package brave.internal;
+package brave.internal.codec;
+
+import brave.internal.Nullable;
/** Internal utility class to validate IPv4 or IPv6 literals */
public final class IpLiteral {
diff --git a/brave/src/main/java/brave/internal/JsonEscaper.java b/brave/src/main/java/brave/internal/codec/JsonEscaper.java
similarity index 98%
rename from brave/src/main/java/brave/internal/JsonEscaper.java
rename to brave/src/main/java/brave/internal/codec/JsonEscaper.java
index 84d103306c..7fde03b97c 100644
--- a/brave/src/main/java/brave/internal/JsonEscaper.java
+++ b/brave/src/main/java/brave/internal/codec/JsonEscaper.java
@@ -11,7 +11,7 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
-package brave.internal;
+package brave.internal.codec;
// Initially, a copy of zipkin2.internal.JsonEscaper
public final class JsonEscaper {
diff --git a/brave/src/main/java/brave/propagation/B3SingleFormat.java b/brave/src/main/java/brave/propagation/B3SingleFormat.java
index c3998342e4..70a95232ef 100644
--- a/brave/src/main/java/brave/propagation/B3SingleFormat.java
+++ b/brave/src/main/java/brave/propagation/B3SingleFormat.java
@@ -19,7 +19,7 @@
import java.nio.ByteBuffer;
import java.util.Collections;
-import static brave.internal.HexCodec.writeHexLong;
+import static brave.internal.codec.HexCodec.writeHexLong;
/**
* This format corresponds to the propagation key "b3" (or "B3"), which delimits fields in the
diff --git a/brave/src/main/java/brave/propagation/TraceContext.java b/brave/src/main/java/brave/propagation/TraceContext.java
index fcf0c97956..b5066e8cab 100644
--- a/brave/src/main/java/brave/propagation/TraceContext.java
+++ b/brave/src/main/java/brave/propagation/TraceContext.java
@@ -21,9 +21,9 @@
import java.util.Collections;
import java.util.List;
-import static brave.internal.HexCodec.lenientLowerHexToUnsignedLong;
-import static brave.internal.HexCodec.toLowerHex;
-import static brave.internal.HexCodec.writeHexLong;
+import static brave.internal.codec.HexCodec.lenientLowerHexToUnsignedLong;
+import static brave.internal.codec.HexCodec.toLowerHex;
+import static brave.internal.codec.HexCodec.writeHexLong;
import static brave.internal.InternalPropagation.FLAG_LOCAL_ROOT;
import static brave.internal.InternalPropagation.FLAG_SAMPLED;
import static brave.internal.InternalPropagation.FLAG_SAMPLED_LOCAL;
diff --git a/brave/src/main/java/brave/propagation/TraceIdContext.java b/brave/src/main/java/brave/propagation/TraceIdContext.java
index 6bfe8b36e8..3d2306f993 100644
--- a/brave/src/main/java/brave/propagation/TraceIdContext.java
+++ b/brave/src/main/java/brave/propagation/TraceIdContext.java
@@ -17,8 +17,8 @@
import brave.internal.Nullable;
import brave.internal.RecyclableBuffers;
-import static brave.internal.HexCodec.toLowerHex;
-import static brave.internal.HexCodec.writeHexLong;
+import static brave.internal.codec.HexCodec.toLowerHex;
+import static brave.internal.codec.HexCodec.writeHexLong;
import static brave.internal.InternalPropagation.FLAG_SAMPLED;
import static brave.internal.InternalPropagation.FLAG_SAMPLED_SET;
diff --git a/brave/src/test/java/brave/features/baggage/SingleHeaderCodec.java b/brave/src/test/java/brave/features/baggage/SingleHeaderCodec.java
index b73abbc767..029efc4ef2 100644
--- a/brave/src/test/java/brave/features/baggage/SingleHeaderCodec.java
+++ b/brave/src/test/java/brave/features/baggage/SingleHeaderCodec.java
@@ -16,6 +16,7 @@
import brave.baggage.BaggageField;
import brave.baggage.BaggageField.ValueUpdater;
import brave.internal.baggage.BaggageCodec;
+import brave.internal.codec.EntrySplitter;
import brave.propagation.TraceContext;
import java.util.Collections;
import java.util.List;
@@ -26,7 +27,8 @@
*
* See https://github.com/w3c/correlation-context/blob/master/correlation_context/HTTP_HEADER_FORMAT.md
*/
-final class SingleHeaderCodec implements BaggageCodec {
+final class SingleHeaderCodec implements BaggageCodec, EntrySplitter.Handler {
+ static final EntrySplitter ENTRY_SPLITTER = EntrySplitter.newBuilder().build();
static final SingleHeaderCodec INSTANCE = new SingleHeaderCodec();
static BaggageCodec get() {
@@ -45,12 +47,14 @@ static BaggageCodec get() {
}
@Override public boolean decode(ValueUpdater valueUpdater, Object request, String value) {
- boolean decoded = false;
- for (String entry : value.split(",", -1)) {
- String[] keyValue = entry.split("=", 2);
- if (valueUpdater.updateValue(BaggageField.create(keyValue[0]), keyValue[1])) decoded = true;
- }
- return decoded;
+ return ENTRY_SPLITTER.parse(this, valueUpdater, value);
+ }
+
+ @Override public boolean onEntry(
+ ValueUpdater target, String buffer, int beginKey, int endKey, int beginValue, int endValue) {
+ BaggageField field = BaggageField.create(buffer.substring(beginKey, endKey));
+ String value = buffer.substring(beginValue, endValue);
+ return target.updateValue(field, value);
}
@Override public String encode(Map values, TraceContext context, Object request) {
diff --git a/brave/src/test/java/brave/internal/PlatformTest.java b/brave/src/test/java/brave/internal/PlatformTest.java
index c89c3c172c..23daf32cf7 100644
--- a/brave/src/test/java/brave/internal/PlatformTest.java
+++ b/brave/src/test/java/brave/internal/PlatformTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2019 The OpenZipkin Authors
+ * Copyright 2013-2020 The OpenZipkin 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
@@ -13,6 +13,7 @@
*/
package brave.internal;
+import brave.internal.codec.HexCodec;
import com.google.common.collect.Sets;
import java.net.Inet6Address;
import java.net.InetAddress;
diff --git a/brave/src/test/java/brave/internal/codec/EntrySplitterTest.java b/brave/src/test/java/brave/internal/codec/EntrySplitterTest.java
new file mode 100644
index 0000000000..bd6d334432
--- /dev/null
+++ b/brave/src/test/java/brave/internal/codec/EntrySplitterTest.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2013-2020 The OpenZipkin 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 brave.internal.codec;
+
+import brave.internal.codec.EntrySplitter.Handler;
+import brave.propagation.TraceContext;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.entry;
+
+public class EntrySplitterTest {
+ EntrySplitter entrySplitter = EntrySplitter.newBuilder().shouldThrow(true).build();
+ Map map = new LinkedHashMap<>();
+ Handler