// 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}.
*
* 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 getData(Object key) {
if (key == null) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("key cannot be null"));
}
return Optional.empty();
}
@Override
public Map 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 valuesMap;
/**
* Constructs a new {@link Context} object.
*
* Code samples
*
*
*
* // 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);
*
*
*
* @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.
*
* Code samples
*
*
*
* // 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());
*
*
*
* @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}.
*
* Code samples
*
*
*
* 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());
*
*
*
* @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 keyValues) {
if (CoreUtils.isNullOrEmpty(keyValues)) {
throw new IllegalArgumentException("Key value map cannot be null or empty");
}
Context context = null;
for (Map.Entry 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.
*
* Code samples
*
*
*
* 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.");
* }
*
*
*
* @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 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.
*
* Code samples
*
*
*
* 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.");
* }
*
*
*
* @return A map containing all values of the context linked-list.
*/
public Map getValues() {
if (valuesMap != null) {
return valuesMap;
}
if (contextCount == 1) {
this.valuesMap = Collections.singletonMap(key, value);
return this.valuesMap;
}
Map 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;
}
}