All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fi.evolver.utils.ContextUtils Maven / Gradle / Ivy

There is a newer version: 3.5.0
Show newest version
package fi.evolver.utils;

import java.util.*;
import java.util.function.Supplier;

import fi.evolver.utils.attribute.ContextAttribute;
import fi.evolver.utils.attribute.ContextRegistrableAttribute;



public class ContextUtils {
	private static final ThreadLocal> contextHolder = new ThreadLocal<>();

	private static final ContextAttribute REGISTERED_ATTRIBUTES = new ContextAttribute<>(
			"%s.%s".formatted(ContextUtils.class.getSimpleName(), ContextLifeCycleAttributes.class.getSimpleName()),
			ContextLifeCycleAttributes.class);


	private ContextUtils() { }


	/**
	 * Query whether we are currently within a context.
	 *
	 * @return True iff there is an enclosing context.
	 */
	public static boolean withinContext() {
		return contextHolder.get() != null;
	}


	/**
	 * Creates a new context if not already within one.
	 *
	 * @return AutoCloseable for closing the context if one was created.
	 */
	public static ContextCloser ensureContext() {
		return ensureContext(null);
	}


	/**
	 * Creates a new context if not already within one, then initiates it with the given values.
	 *
	 * @param context Base for the current context.
	 * @return AutoCloseable for closing the context if one was created.
	 */
	public static ContextCloser ensureContext(Context context) {
		boolean withinContext = withinContext();
		if (!withinContext)
			initContext();
		if (context != null) {
			contextHolder.get().putAll(context.data);
			REGISTERED_ATTRIBUTES.computeIfPresent(ContextLifeCycleAttributes::copy);
			for (ContextRegistrableAttribute attribute: List.copyOf(getRegisteredAttributes().values()))
				setAttributeValue(attribute, context.attributeValues.get(attribute.name()));
		}
		return new ContextCloser(!withinContext);
	}


	private static  void setAttributeValue(ContextRegistrableAttribute attribute, Object value) {
		attribute.set(attribute.type().cast(value));
	}


	/**
	 * Creates a new context. Normally using {@link #ensureContext} in a try-with-resources is cleaner.
	 * @throws IllegalStateException if already inside context
	 */
	public static void createContext() {
		if (withinContext())
			throw new IllegalStateException("Already inside context!");
		initContext();
	}


	private static void initContext() {
		contextHolder.set(new LinkedHashMap<>());
	}


	/**
	 * Get a value from within the current context if available or empty
	 * if not within a context or no value was found.
	 *
	 * @param key The key for the value to get.
	 * @param clazz The type of the requested value.
	 * @return The value if found cast to the given type.
	 */
	public static  Optional get(String key, Class clazz) {
		return Optional.ofNullable(contextHolder.get())
				.map(c -> c.get(key))
				.map(clazz::cast);
	}


	/**
	 * Get a value from within the current context if available or the given
	 * default value if not within a context or no value was found.
	 *
	 * @param key The key for the value to get.
	 * @param clazz The type of the requested value.
	 * @param defaultValue Default value to return when no context or value is found.
	 * @return The found value or the given default value.
	 */
	public static  T getOrDefault(String key, Class clazz, T defaultValue) {
		return Optional.ofNullable(contextHolder.get())
				.map(c -> c.get(key))
				.filter(Objects::nonNull)
				.filter(c -> clazz.isAssignableFrom(c.getClass()))
				.map(clazz::cast)
				.orElse(defaultValue);
	}


	/**
	 * Get a value from within the current context if available, or compute
	 * and put otherwise. Skips the put phase if not within a context.
	 *
	 * @param key The key for the value to get.
	 * @param clazz The type of the requested value.
	 * @param defaultSupplier Supplier for the default value.
	 * @return The found value or the given default value.
	 */
	public static  T computeIfAbsent(String key, Class clazz, Supplier defaultSupplier) {
		Optional value = Optional.ofNullable(contextHolder.get())
				.map(c -> c.get(key))
				.filter(Objects::nonNull)
				.filter(c -> clazz.isAssignableFrom(c.getClass()))
				.map(clazz::cast);

		if (value.isPresent())
			return value.get();

		T computed = defaultSupplier.get();
		if (computed != null && withinContext())
			put(key, computed);
		return computed;
	}


	/**
	 * Puts the given value into the current context. Fails if not within a context.
	 * A null value removes the attribute from the context.
	 *
	 * @param key The key for the value to add.
	 * @param value The value to add to the current context.
	 */
	public static void put(String key, Object value) {
		if (!withinContext())
			throw new IllegalStateException("No context available");

		if (value == null)
			remove(key);
		else
			contextHolder.get().put(key, value);
	}


	/**
	 * Puts the contents of given map into the current context. Fails if not within a context.
	 *
	 * @param values The values to put to the context.
	 */
	public static void putAll(Map values) {
		if (!withinContext())
			throw new IllegalStateException("No context available");
		contextHolder.get().putAll(values);
	}


	/**
	 * Removes the given key from the current context.
	 *
	 * @param key The key to remove.
	 */
	public static void remove(String key) {
		if (withinContext())
			contextHolder.get().remove(key);
	}


	/**
	 * Destroys the current context, if any. Usually the cleanup is handled by the AutoCloseable of the outermost
	 * ensureContext if used in a try-with-resources block.
	 */
	public static void destroyContext() {
		if (!withinContext())
			return;

		for (ContextRegistrableAttribute attribute: getRegisteredAttributes().values())
			attribute.remove();
		contextHolder.remove();
	}


	/**
	 * Capture the current context for migrating to another thread.
	 *
	 * @return The current context.
	 */
	public static Context getContext() {
		return new Context(contextHolder.get());
	}


	/**
	 * Register an attribute for life-cycle handling by the current context.
	 *
	 * The attribute will be:
	 *  - migrated to other threads with the context
	 *  - removed (for a thread) when the context is closed
	 *
	 * @param attribute The attribute to register.
	 */
	public static void register(ContextRegistrableAttribute attribute) {
		if (!withinContext())
			throw new IllegalStateException("No context available");

		Objects.requireNonNull(attribute, "Cannot register null attribute");

		ContextRegistrableAttribute oldAttribute = getRegisteredAttributes().get(attribute.name());

		if (oldAttribute != null && oldAttribute != attribute)
			throw new IllegalStateException("Cannot register multiple attributes with the same name %s: %s && %s".formatted(
					attribute.name(), attribute, oldAttribute));

		getRegisteredAttributes().put(attribute.name(), attribute);
	}


	private static ContextLifeCycleAttributes getRegisteredAttributes() {
		return REGISTERED_ATTRIBUTES.computeIfAbsent(ContextLifeCycleAttributes::new);
	}


	public static class ContextCloser implements AutoCloseable {
		private final boolean destroyContext;

		private ContextCloser(boolean destroyContext) {
			this.destroyContext = destroyContext;
		}

		@Override
		public void close() {
			if (destroyContext)
				destroyContext();
		}
	}


	/**
	 * A holder for migrating context to another thread.
	 */
	public static class Context {
		private final Map data;
		private final Map attributeValues;


		private Context(Map data) {
			this.data = new LinkedHashMap<>();
			if (data != null)
				this.data.putAll(data);

			ContextLifeCycleAttributes registeredAttributes = getRegisteredAttributes().copy();
			this.data.put(REGISTERED_ATTRIBUTES.name(), registeredAttributes);

			this.attributeValues = new LinkedHashMap<>();
			for (ContextRegistrableAttribute attribute: registeredAttributes.values())
				attribute.get().ifPresent(v -> attributeValues.put(attribute.name(), v));
		}


		@Override
		public String toString() {
			return "Context [data=" + data + ", attributeValues=" + attributeValues + "]";
		}
	}


	private static class ContextLifeCycleAttributes extends LinkedHashMap> {
		private static final long serialVersionUID = 1L;

		private ContextLifeCycleAttributes copy() {
			ContextLifeCycleAttributes copy = new ContextLifeCycleAttributes();
			copy.putAll(this);
			return copy;
		}
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy