-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
Context.java
315 lines (282 loc) · 12 KB
/
Context.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.core.util;
import com.azure.core.annotation.Immutable;
import com.azure.core.util.logging.ClientLogger;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* {@code Context} offers a means of passing arbitrary data (key-value pairs) to pipeline policies.
* Most applications do not need to pass arbitrary data to the pipeline and can pass {@code Context.NONE} or
* {@code null}.
* <p>
* Each context object is immutable. The {@link #addData(Object, Object)} method creates a new
* {@code Context} object that refers to its parent, forming a linked list.
*/
@Immutable
public class Context {
private static final ClientLogger LOGGER = new ClientLogger(Context.class);
private static final Context[] EMPTY_CHAIN = new Context[0];
// All fields must be immutable.
//
/**
* Signifies that no data needs to be passed to the pipeline.
*/
public static final Context NONE = new Context(null, null, null, 0) {
@Override
public Optional<Object> getData(Object key) {
if (key == null) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("key cannot be null"));
}
return Optional.empty();
}
@Override
public Map<Object, Object> getValues() {
return Collections.emptyMap();
}
@Override
Context[] getContextChain() {
return EMPTY_CHAIN;
}
};
private final Context parent;
private final Object key;
private final Object value;
private final int contextCount;
private Map<Object, Object> valuesMap;
/**
* Constructs a new {@link Context} object.
*
* <p><strong>Code samples</strong></p>
*
* <!-- src_embed com.azure.core.util.context#object-object -->
* <pre>
* // Create an empty context having no data
* Context emptyContext = Context.NONE;
*
* // OpenTelemetry context can be optionally passed using PARENT_TRACE_CONTEXT_KEY
* // when OpenTelemetry context is not provided explicitly, ambient
* // io.opentelemetry.context.Context.current() is used
*
* // Context contextWithSpan = new Context(PARENT_TRACE_CONTEXT_KEY, openTelemetryContext);
* </pre>
* <!-- end com.azure.core.util.context#object-object -->
*
* @param key The key with which the specified value should be associated.
* @param value The value to be associated with the specified key.
* @throws IllegalArgumentException If {@code key} is {@code null}.
*/
public Context(Object key, Object value) {
this.parent = null;
this.key = Objects.requireNonNull(key, "'key' cannot be null.");
this.value = value;
this.contextCount = 1;
}
private Context(Context parent, Object key, Object value, int contextCount) {
this.parent = parent;
this.key = key;
this.value = value;
this.contextCount = contextCount;
}
Object getKey() {
return key;
}
Object getValue() {
return value;
}
/**
* Adds a new immutable {@link Context} object with the specified key-value pair to
* the existing {@link Context} chain.
*
* <p><strong>Code samples</strong></p>
*
* <!-- src_embed com.azure.core.util.context.addData#object-object -->
* <pre>
* // Users can pass parent trace context information and additional metadata to attach to spans created by SDKs
* // using the com.azure.core.util.Context object.
* final String hostNameValue = "host-name-value";
* final String entityPathValue = "entity-path-value";
*
* // TraceContext represents a tracing solution context type - io.opentelemetry.context.Context for OpenTelemetry.
* final TraceContext parentContext = TraceContext.root();
* Context parentSpanContext = new Context(PARENT_TRACE_CONTEXT_KEY, parentContext);
*
* // Add a new key value pair to the existing context object.
* Context updatedContext = parentSpanContext.addData(HOST_NAME_KEY, hostNameValue)
* .addData(ENTITY_PATH_KEY, entityPathValue);
*
* // Both key values found on the same updated context object
* System.out.printf("Hostname value: %s%n", updatedContext.getData(HOST_NAME_KEY).get());
* System.out.printf("Entity Path value: %s%n", updatedContext.getData(ENTITY_PATH_KEY).get());
* </pre>
* <!-- end com.azure.core.util.context.addData#object-object -->
*
* @param key The key with which the specified value should be associated.
* @param value The value to be associated with the specified key.
* @return the new {@link Context} object containing the specified pair added to the set of pairs.
* @throws IllegalArgumentException If {@code key} is {@code null}.
*/
public Context addData(Object key, Object value) {
if (key == null) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("key cannot be null"));
}
return new Context(this, key, value, contextCount + 1);
}
/**
* Creates a new immutable {@link Context} object with all the keys and values provided by
* the input {@link Map}.
*
* <p><strong>Code samples</strong></p>
*
* <!-- src_embed com.azure.core.util.context.of#map -->
* <pre>
* final String key1 = "Key1";
* final String value1 = "first-value";
* Map<Object, Object> keyValueMap = new HashMap<>();
* keyValueMap.put(key1, value1);
*
* // Create a context using the provided key value pair map
* Context keyValueContext = Context.of(keyValueMap);
* System.out.printf("Key1 value %s%n", keyValueContext.getData(key1).get());
* </pre>
* <!-- end com.azure.core.util.context.of#map -->
*
* @param keyValues The input key value pairs that will be added to this context.
* @return Context object containing all the key-value pairs in the input map.
* @throws IllegalArgumentException If {@code keyValues} is {@code null} or empty
*/
public static Context of(Map<Object, Object> keyValues) {
if (CoreUtils.isNullOrEmpty(keyValues)) {
throw new IllegalArgumentException("Key value map cannot be null or empty");
}
Context context = null;
for (Map.Entry<Object, Object> entry : keyValues.entrySet()) {
if (context == null) {
context = new Context(entry.getKey(), entry.getValue());
} else {
context = context.addData(entry.getKey(), entry.getValue());
}
}
return context;
}
/**
* Scans the linked-list of {@link Context} objects looking for one with the specified key.
* Note that the first key found, i.e. the most recently added, will be returned.
*
* <p><strong>Code samples</strong></p>
*
* <!-- src_embed com.azure.core.util.context.getData#object -->
* <pre>
* final String key1 = "Key1";
* final String value1 = "first-value";
*
* // Create a context object with given key and value
* Context context = new Context(key1, value1);
*
* // Look for the specified key in the returned context object
* Optional<Object> optionalObject = context.getData(key1);
* if (optionalObject.isPresent()) {
* System.out.printf("Key1 value: %s%n", optionalObject.get());
* } else {
* System.out.println("Key1 does not exist or have data.");
* }
* </pre>
* <!-- end com.azure.core.util.context.getData#object -->
*
* @param key The key to search for.
* @return The value of the specified key if it exists.
* @throws IllegalArgumentException If {@code key} is {@code null}.
*/
public Optional<Object> getData(Object key) {
if (key == null) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("key cannot be null"));
}
for (Context c = this; c != null; c = c.parent) {
if (key.equals(c.key)) {
return Optional.ofNullable(c.value);
}
// If the contextCount is 1 that means the next parent Context is the NONE Context.
// Return Optional.empty now to prevent a meaningless check.
if (c.contextCount == 1) {
return Optional.empty();
}
}
// This should never be reached but is required by the compiler.
return Optional.empty();
}
/**
* Scans the linked-list of {@link Context} objects populating a {@link Map} with the values of the context.
*
* <p><strong>Code samples</strong></p>
*
* <!-- src_embed com.azure.core.util.Context.getValues -->
* <pre>
* final String key1 = "Key1";
* final String value1 = "first-value";
* final String key2 = "Key2";
* final String value2 = "second-value";
*
* Context context = new Context(key1, value1)
* .addData(key2, value2);
*
* Map<Object, Object> contextValues = context.getValues();
* if (contextValues.containsKey(key1)) {
* System.out.printf("Key1 value: %s%n", contextValues.get(key1));
* } else {
* System.out.println("Key1 does not exist.");
* }
*
* if (contextValues.containsKey(key2)) {
* System.out.printf("Key2 value: %s%n", contextValues.get(key2));
* } else {
* System.out.println("Key2 does not exist.");
* }
* </pre>
* <!-- end com.azure.core.util.Context.getValues -->
*
* @return A map containing all values of the context linked-list.
*/
public Map<Object, Object> getValues() {
if (valuesMap != null) {
return valuesMap;
}
if (contextCount == 1) {
this.valuesMap = Collections.singletonMap(key, value);
return this.valuesMap;
}
Map<Object, Object> map = new HashMap<>((int) Math.ceil(contextCount / 0.75F));
for (Context pointer = this; pointer != null; pointer = pointer.parent) {
if (pointer.key != null) {
map.putIfAbsent(pointer.key, pointer.value);
}
// If the contextCount is 1 that means the next parent Context is the NONE Context.
// Break out of the loop to prevent a meaningless check.
if (pointer.contextCount == 1) {
break;
}
}
this.valuesMap = Collections.unmodifiableMap(map);
return this.valuesMap;
}
/**
* Gets the {@link Context Contexts} in the chain of Contexts that this Context is the tail.
*
* @return The Contexts, in oldest to newest order, in the chain of Contexts that this Context is the tail.
*/
Context[] getContextChain() {
Context[] chain = new Context[contextCount];
int chainPosition = contextCount - 1;
for (Context pointer = this; pointer != null; pointer = pointer.parent) {
chain[chainPosition--] = pointer;
// If the contextCount is 1 that means the next parent Context is the NONE Context.
// Break out of the loop to prevent a meaningless check.
if (pointer.contextCount == 1) {
break;
}
}
return chain;
}
}