11package org .hypertrace .core .grpcutils .context ;
22
33import static io .grpc .Metadata .ASCII_STRING_MARSHALLER ;
4+ import static java .util .Objects .requireNonNull ;
45import static org .hypertrace .core .grpcutils .context .RequestContextConstants .CACHE_MEANINGFUL_HEADERS ;
56import static org .hypertrace .core .grpcutils .context .RequestContextConstants .TENANT_ID_HEADER_KEY ;
67
7- import com .google .common .collect .Maps ;
8+ import com .google .common .collect .ListMultimap ;
9+ import com .google .common .collect .Multimap ;
10+ import com .google .common .collect .MultimapBuilder ;
11+ import com .google .common .collect .Multimaps ;
812import io .grpc .Context ;
913import io .grpc .Metadata ;
1014import io .grpc .Metadata .Key ;
1115import java .nio .charset .StandardCharsets ;
1216import java .util .Collections ;
13- import java .util .HashMap ;
1417import java .util .List ;
1518import java .util .Map ;
1619import java .util .Optional ;
20+ import java .util .Set ;
1721import java .util .UUID ;
1822import java .util .concurrent .Callable ;
23+ import java .util .stream .Collectors ;
1924import javax .annotation .Nonnull ;
25+ import lombok .Value ;
2026
2127/**
2228 * Context of the GRPC request that should be carried and can made available to the services so that
@@ -26,10 +32,9 @@ public class RequestContext {
2632 public static final Context .Key <RequestContext > CURRENT = Context .key ("request_context" );
2733
2834 public static RequestContext forTenantId (String tenantId ) {
29- RequestContext requestContext = new RequestContext ();
30- requestContext .add (RequestContextConstants .TENANT_ID_HEADER_KEY , tenantId );
31- requestContext .add (RequestContextConstants .REQUEST_ID_HEADER_KEY , UUID .randomUUID ().toString ());
32- return requestContext ;
35+ return new RequestContext ()
36+ .put (RequestContextConstants .TENANT_ID_HEADER_KEY , tenantId )
37+ .put (RequestContextConstants .REQUEST_ID_HEADER_KEY , UUID .randomUUID ().toString ());
3338 }
3439
3540 public static RequestContext fromMetadata (Metadata metadata ) {
@@ -53,19 +58,20 @@ public static RequestContext fromMetadata(Metadata metadata) {
5358 }
5459 // The value could be null or empty for some keys so validate that.
5560 if (value != null && !value .isEmpty ()) {
56- requestContext .add (k , value );
61+ requestContext .put (k , value );
5762 }
5863 });
5964
6065 return requestContext ;
6166 }
6267
63- private final Map <String , String > headers = new HashMap <>();
68+ private final ListMultimap <String , RequestContextHeader > headers =
69+ MultimapBuilder .linkedHashKeys ().linkedListValues ().build ();
6470 private final JwtParser jwtParser = new JwtParser ();
6571
6672 /** Reads tenant id from this RequestContext based on the tenant id http header and returns it. */
6773 public Optional <String > getTenantId () {
68- return get (RequestContextConstants .TENANT_ID_HEADER_KEY );
74+ return getHeaderValue (RequestContextConstants .TENANT_ID_HEADER_KEY );
6975 }
7076
7177 public Optional <String > getUserId () {
@@ -96,28 +102,110 @@ public Optional<JwtClaim> getClaim(String claimName) {
96102 }
97103
98104 public Optional <String > getRequestId () {
99- return this .get (RequestContextConstants .REQUEST_ID_HEADER_KEY );
105+ return this .getHeaderValue (RequestContextConstants .REQUEST_ID_HEADER_KEY );
100106 }
101107
102108 private Optional <Jwt > getJwt () {
103- return get (RequestContextConstants .AUTHORIZATION_HEADER ).flatMap (jwtParser ::fromAuthHeader );
109+ return this .getHeaderValue (RequestContextConstants .AUTHORIZATION_HEADER )
110+ .flatMap (jwtParser ::fromAuthHeader );
104111 }
105112
106- /** Method to read all GRPC request headers from this RequestContext. */
113+ /**
114+ * This is retained for backwards compatibility, but is based on the incorrect assumption that a
115+ * header only can have one value. For the updated API, please use {@link #getAllHeaders()}}
116+ */
117+ @ Deprecated
107118 public Map <String , String > getRequestHeaders () {
108119 return getAll ();
109120 }
110121
111- public void add (String headerKey , String headerValue ) {
112- this .headers .put (headerKey , headerValue );
122+ /**
123+ * This is retained for backwards compatibility. It previously was implemented with the assumption
124+ * of a single value per header, so overwrote on addition. This is maintained by first removing
125+ * any values for the given header name, then adding
126+ *
127+ * @param headerName
128+ * @param headerValue
129+ */
130+ @ Deprecated
131+ public void add (String headerName , String headerValue ) {
132+ this .removeHeader (headerName );
133+ this .put (headerName , headerValue );
113134 }
114135
115- public Optional <String > get (String headerKey ) {
116- return Optional .ofNullable (this .headers .get (headerKey ));
136+ /** Prefer {@link #getHeaderValue} */
137+ @ Deprecated
138+ public Optional <String > get (String headerName ) {
139+ return this .getHeaderValue (headerName );
117140 }
118141
142+ /**
143+ * This is retained for backwards compatibility, but is based on the incorrect assumption that a
144+ * header only can have one value. For the updated API, please use {@link #getAllHeaders()} ()}
145+ */
146+ @ Deprecated
119147 public Map <String , String > getAll () {
120- return Map .copyOf (headers );
148+ return this .getHeaderNames ().stream ()
149+ .flatMap (key -> this .headers .get (key ).stream ().findFirst ().stream ())
150+ .collect (
151+ Collectors .toUnmodifiableMap (
152+ RequestContextHeader ::getName , RequestContextHeader ::getValue ));
153+ }
154+
155+ /**
156+ * Adds the provided header name and header value. Duplicates are allowed. For fluency, the
157+ * current instance is returned.
158+ */
159+ public RequestContext put (String headerName , String headerValue ) {
160+ this .headers .put (
161+ this .normalizeHeaderName (requireNonNull (headerName )),
162+ new RequestContextHeader (headerName , headerValue ));
163+ return this ;
164+ }
165+
166+ /** Returns all header names in normalized form (case not preserved) */
167+ @ Nonnull
168+ public Set <String > getHeaderNames () {
169+ return Set .copyOf (this .headers .keySet ());
170+ }
171+
172+ /**
173+ * Removes and returns all headers matching the provided name. Header names are case insensitive.
174+ * Returns an empty list if no headers have been removed.
175+ */
176+ @ Nonnull
177+ public List <String > removeHeader (String name ) {
178+ return this .headers .removeAll (this .normalizeHeaderName (name )).stream ()
179+ .map (RequestContextHeader ::getValue )
180+ .collect (Collectors .toUnmodifiableList ());
181+ }
182+
183+ /** Returns all header values matching the provided header name, case insensitively. */
184+ @ Nonnull
185+ public List <String > getAllHeaderValues (String key ) {
186+ return this .headers .get (this .normalizeHeaderName (key )).stream ()
187+ .map (RequestContextHeader ::getValue )
188+ .collect (Collectors .toUnmodifiableList ());
189+ }
190+
191+ /**
192+ * Returns all header name-value pairs, with the original case preserved. Multiple headers with
193+ * the same name, and potentially same value may be returned.
194+ */
195+ @ Nonnull
196+ public List <RequestContextHeader > getAllHeaders () {
197+ return List .copyOf (this .headers .values ());
198+ }
199+
200+ /**
201+ * Gets the first header value specified for the provided name (case insensitive), or empty if no
202+ * match.
203+ */
204+ @ Nonnull
205+ public Optional <String > getHeaderValue (String key ) {
206+ return this .headers .get (this .normalizeHeaderName (key )).stream ()
207+ .map (RequestContextHeader ::getValue )
208+ .findFirst ();
121209 }
122210
123211 public <V > V call (@ Nonnull Callable <V > callable ) {
@@ -198,8 +286,12 @@ public Metadata buildTrailers() {
198286 return trailers ;
199287 }
200288
201- private Map <String , String > getHeadersOtherThanAuth () {
202- return Maps .filterKeys (
289+ private String normalizeHeaderName (@ Nonnull String headerName ) {
290+ return headerName .toLowerCase ();
291+ }
292+
293+ private Multimap <String , RequestContextHeader > getHeadersOtherThanAuth () {
294+ return Multimaps .filterKeys (
203295 headers , key -> !key .equals (RequestContextConstants .AUTHORIZATION_HEADER ));
204296 }
205297
@@ -213,4 +305,10 @@ public String toString() {
213305 + getJwt ().map (Jwt ::toString ).orElse (emptyValue )
214306 + '}' ;
215307 }
308+
309+ @ Value
310+ public static class RequestContextHeader {
311+ String name ;
312+ String value ;
313+ }
216314}
0 commit comments