Observer.java

/*
 * Copyright 2025-present the original author or 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
 *
 *      https://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 org.springframework.data.mongodb.observability;

import io.micrometer.common.KeyValues;
import io.micrometer.common.docs.KeyName;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;

import org.jspecify.annotations.Nullable;

/**
 * An observer abstraction that can observe a context and contribute {@literal KeyValue}s for propagation into
 * observability systems.
 *
 * @author Mark Paluch
 * @since 4.4.9
 * @deprecated since 5.1 in favor of native MongoDB Java Driver observability support.
 */
@Deprecated(since = "5.1",  forRemoval = true)
class Observer {

	private final List<MongoKeyName.MongoKeyValue> keyValues = new ArrayList<>();

	/**
	 * Create a new {@link Observer}.
	 *
	 * @return a new {@link Observer}.
	 */
	static Observer create() {
		return new Observer();
	}

	/**
	 * Create a new {@link Observer} given an optional context and a consumer that will contribute key-value tuples from
	 * the given context.
	 *
	 * @param context the context to observe, can be {@literal null}.
	 * @param consumer consumer for a functional declaration that supplies key-value tuples.
	 * @return the stateful {@link Observer}.
	 * @param <C> context type.
	 */
	static <C> Observer fromContext(@Nullable C context, Consumer<? super ContextualObserver<C>> consumer) {

		Observer contributor = create();

		consumer.accept(contributor.contextual(context));

		return contributor;
	}

	/**
	 * Contribute a single {@link MongoKeyName.MongoKeyValue} to the observer.
	 *
	 * @param keyValue
	 * @return
	 */
	Observer contribute(MongoKeyName.MongoKeyValue keyValue) {

		keyValues.add(keyValue);

		return this;
	}

	/**
	 * Create a nested, contextual {@link ContextualObserver} that can contribute key-value tuples based on the given
	 * context.
	 *
	 * @param context the context to observe, can be {@literal null}.
	 * @return the nested contextual {@link ContextualObserver} that can contribute key-value tuples.
	 * @param <C>
	 */
	<C> ContextualObserver<C> contextual(@Nullable C context) {

		if (context == null) {
			return new EmptyContextualObserver<>(keyValues);
		}

		return new DefaultContextualObserver<>(context, keyValues);
	}

	KeyValues toKeyValues() {
		return KeyValues.of(keyValues);
	}

	KeyName[] toKeyNames() {

		KeyName[] keyNames = new KeyName[keyValues.size()];

		for (int i = 0; i < keyValues.size(); i++) {
			MongoKeyName.MongoKeyValue keyValue = keyValues.get(i);
			keyNames[i] = keyValue;
		}

		return keyNames;
	}

	/**
	 * Contextual observer interface to contribute key-value tuples based on a context. The context can be transformed
	 * into a nested context using {@link #nested(Function)}.
	 *
	 * @param <T>
	 */
	interface ContextualObserver<T> {

		/**
		 * Create a nested {@link ContextualObserver} that can contribute key-value tuples based on the transformation of
		 * the current context. If the {@code mapper} function returns {@literal null}, the nested observer will operate
		 * without a context contributing {@literal MonKoKeyName.absent()} values simplifying nullability handling.
		 *
		 * @param mapper context mapper function that transforms the current context into a nested context.
		 * @return the nested contextual observer.
		 * @param <N> nested context type.
		 */
		<N> ContextualObserver<N> nested(Function<? super T, ? extends @Nullable N> mapper);

		/**
		 * Functional-style contribution of a {@link ContextualObserver} callback.
		 *
		 * @param consumer the consumer that will be invoked with this {@link ContextualObserver}.
		 * @return {@code this} {@link ContextualObserver} for further chaining.
		 */
		default ContextualObserver<T> contribute(Consumer<? super ContextualObserver<T>> consumer) {
			consumer.accept(this);
			return this;
		}

		/**
		 * Contribute a {@link MongoKeyName.MongoKeyValue} to the observer.
		 *
		 * @param keyValue
		 * @return {@code this} {@link ContextualObserver} for further chaining.
		 */
		ContextualObserver<T> contribute(MongoKeyName.MongoKeyValue keyValue);

		/**
		 * Contribute a {@link MongoKeyName} to the observer.
		 *
		 * @param keyName
		 * @return {@code this} {@link ContextualObserver} for further chaining.
		 */
		default ContextualObserver<T> contribute(MongoKeyName<T> keyName) {
			return contribute(List.of(keyName));
		}

		/**
		 * Contribute a collection of {@link MongoKeyName}s to the observer.
		 *
		 * @param keyName0
		 * @param keyName1
		 * @return {@code this} {@link ContextualObserver} for further chaining.
		 */
		default ContextualObserver<T> contribute(MongoKeyName<T> keyName0, MongoKeyName<T> keyName1) {
			return contribute(List.of(keyName0, keyName1));
		}

		/**
		 * Contribute a collection of {@link MongoKeyName}s to the observer.
		 *
		 * @param keyName0
		 * @param keyName1
		 * @param keyName2
		 * @return {@code this} {@link ContextualObserver} for further chaining.
		 */
		default ContextualObserver<T> contribute(MongoKeyName<T> keyName0, MongoKeyName<T> keyName1,
				MongoKeyName<T> keyName2) {
			return contribute(List.of(keyName0, keyName1, keyName2));
		}

		/**
		 * Contribute a collection of {@link MongoKeyName}s to the observer.
		 *
		 * @param keyNames
		 * @return {@code this} {@link ContextualObserver} for further chaining.
		 */
		ContextualObserver<T> contribute(Iterable<MongoKeyName<T>> keyNames);

	}

	/**
	 * A default {@link ContextualObserver} that observes a target and contributes key-value tuples by providing the
	 * context to {@link MongoKeyName}.
	 *
	 * @param target
	 * @param keyValues
	 * @param <T>
	 */
	private record DefaultContextualObserver<T>(T target,
			List<MongoKeyName.MongoKeyValue> keyValues) implements ContextualObserver<T> {

		public <N> ContextualObserver<N> nested(Function<? super T, ? extends @Nullable N> mapper) {

			N nestedTarget = mapper.apply(target);

			if (nestedTarget == null) {
				return new EmptyContextualObserver<>(keyValues);
			}

			return new DefaultContextualObserver<>(nestedTarget, keyValues);
		}

		@Override
		public ContextualObserver<T> contribute(MongoKeyName.MongoKeyValue keyValue) {

			keyValues.add(keyValue);

			return this;
		}

		@Override
		public ContextualObserver<T> contribute(MongoKeyName<T> keyName) {

			keyValues.add(keyName.valueOf(target));

			return this;
		}

		@Override
		public ContextualObserver<T> contribute(Iterable<MongoKeyName<T>> keyNames) {

			for (MongoKeyName<T> name : keyNames) {
				keyValues.add(name.valueOf(target));
			}

			return this;
		}

	}

	/**
	 * Empty {@link ContextualObserver} that is not associated with a context and therefore, it only contributes
	 * {@link MongoKeyName#absent()} values.
	 *
	 * @param keyValues
	 * @param <T>
	 */
	private record EmptyContextualObserver<T>(
			List<MongoKeyName.MongoKeyValue> keyValues) implements ContextualObserver<T> {

		public <N> ContextualObserver<N> nested(Function<? super T, ? extends @Nullable N> mapper) {
			return new EmptyContextualObserver<>(keyValues);
		}

		@Override
		public ContextualObserver<T> contribute(MongoKeyName.MongoKeyValue keyValue) {

			keyValues.add(keyValue);

			return this;
		}

		@Override
		public ContextualObserver<T> contribute(Iterable<MongoKeyName<T>> keyNames) {

			for (MongoKeyName<T> name : keyNames) {
				keyValues.add(name.absent());
			}

			return this;
		}

	}

}