QueryCharacteristics.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.core.schema;

import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.bson.BsonNull;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.Bound;
import org.springframework.data.util.Streamable;

/**
 * Encapsulation of individual {@link QueryCharacteristic query characteristics} used to define queries that can be
 * executed when using queryable encryption.
 *
 * @author Christoph Strobl
 * @since 4.5
 */
public class QueryCharacteristics implements Streamable<QueryCharacteristic> {

	/**
	 * instance indicating none
	 */
	private static final QueryCharacteristics NONE = new QueryCharacteristics(Collections.emptyList());

	private final List<QueryCharacteristic> characteristics;

	QueryCharacteristics(List<QueryCharacteristic> characteristics) {
		this.characteristics = characteristics;
	}

	/**
	 * @return marker instance indicating no characteristics have been defined.
	 */
	public static QueryCharacteristics none() {
		return NONE;
	}

	/**
	 * Create new {@link QueryCharacteristics} from given list of {@link QueryCharacteristic characteristics}.
	 *
	 * @param characteristics must not be {@literal null}.
	 * @return new instance of {@link QueryCharacteristics}.
	 */
	public static QueryCharacteristics of(List<QueryCharacteristic> characteristics) {
		return new QueryCharacteristics(List.copyOf(characteristics));
	}

	/**
	 * Create new {@link QueryCharacteristics} from given {@link QueryCharacteristic characteristics}.
	 *
	 * @param characteristics must not be {@literal null}.
	 * @return new instance of {@link QueryCharacteristics}.
	 */
	public static QueryCharacteristics of(QueryCharacteristic... characteristics) {
		return new QueryCharacteristics(Arrays.asList(characteristics));
	}

	/**
	 * @return the list of {@link QueryCharacteristic characteristics}.
	 */
	public List<QueryCharacteristic> getCharacteristics() {
		return characteristics;
	}

	@Override
	public Iterator<QueryCharacteristic> iterator() {
		return this.characteristics.iterator();
	}

	/**
	 * Create a new {@link RangeQuery range query characteristic} used to define range queries against an encrypted field.
	 *
	 * @param <T> targeted field type
	 * @return new instance of {@link RangeQuery}.
	 */
	public static <T> RangeQuery<T> range() {
		return new RangeQuery<>();
	}

	/**
	 * Create a new {@link EqualityQuery equality query characteristic} used to define equality queries against an
	 * encrypted field.
	 *
	 * @param <T> targeted field type
	 * @return new instance of {@link EqualityQuery}.
	 */
	public static <T> EqualityQuery<T> equality() {
		return new EqualityQuery<>(null);
	}

	/**
	 * {@link QueryCharacteristic} for equality comparison.
	 *
	 * @param <T>
	 * @since 4.5
	 */
	public static class EqualityQuery<T> implements QueryCharacteristic {

		private final @Nullable Long contention;

		/**
		 * Create new instance of {@link EqualityQuery}.
		 *
		 * @param contention can be {@literal null}.
		 */
		public EqualityQuery(@Nullable Long contention) {
			this.contention = contention;
		}

		/**
		 * @param contention concurrent counter partition factor.
		 * @return new instance of {@link EqualityQuery}.
		 */
		public EqualityQuery<T> contention(long contention) {
			return new EqualityQuery<>(contention);
		}

		@Override
		public String queryType() {
			return "equality";
		}

		@Override
		public Document toDocument() {
			return QueryCharacteristic.super.toDocument().append("contention", contention);
		}
	}

	/**
	 * {@link QueryCharacteristic} for range comparison.
	 *
	 * @param <T>
	 * @since 4.5
	 */
	public static class RangeQuery<T> implements QueryCharacteristic {

		private final @Nullable Range<T> valueRange;
		private final @Nullable Integer trimFactor;
		private final @Nullable Long sparsity;
		private final @Nullable Long precision;
		private final @Nullable Long contention;

		private RangeQuery() {
			this(Range.unbounded(), null, null, null, null);
		}

		/**
		 * Create new instance of {@link RangeQuery}.
		 *
		 * @param valueRange
		 * @param trimFactor
		 * @param sparsity
		 * @param contention
		 */
		public RangeQuery(@Nullable Range<T> valueRange, @Nullable Integer trimFactor, @Nullable Long sparsity,
				@Nullable Long precision, @Nullable Long contention) {
			this.valueRange = valueRange;
			this.trimFactor = trimFactor;
			this.sparsity = sparsity;
			this.precision = precision;
			this.contention = contention;
		}

		/**
		 * @param lower the lower value range boundary for the queryable field.
		 * @return new instance of {@link RangeQuery}.
		 */
		public RangeQuery<T> min(T lower) {

			Range<T> range = Range.of(Bound.inclusive(lower),
					valueRange != null ? valueRange.getUpperBound() : Bound.unbounded());
			return new RangeQuery<>(range, trimFactor, sparsity, precision, contention);
		}

		/**
		 * @param upper the upper value range boundary for the queryable field.
		 * @return new instance of {@link RangeQuery}.
		 */
		public RangeQuery<T> max(T upper) {

			Range<T> range = Range.of(valueRange != null ? valueRange.getLowerBound() : Bound.unbounded(),
					Bound.inclusive(upper));
			return new RangeQuery<>(range, trimFactor, sparsity, precision, contention);
		}

		/**
		 * @param trimFactor value to control the throughput of concurrent inserts and updates.
		 * @return new instance of {@link RangeQuery}.
		 */
		public RangeQuery<T> trimFactor(int trimFactor) {
			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
		}

		/**
		 * @param sparsity value to control the value density within the index.
		 * @return new instance of {@link RangeQuery}.
		 */
		public RangeQuery<T> sparsity(long sparsity) {
			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
		}

		/**
		 * @param contention concurrent counter partition factor.
		 * @return new instance of {@link RangeQuery}.
		 */
		public RangeQuery<T> contention(long contention) {
			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
		}

		/**
		 * @param precision digits considered comparing floating point numbers.
		 * @return new instance of {@link RangeQuery}.
		 */
		public RangeQuery<T> precision(long precision) {
			return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention);
		}

		@Override
		public String queryType() {
			return "range";
		}

		@Override
		@SuppressWarnings("unchecked")
		public Document toDocument() {

			Document target = QueryCharacteristic.super.toDocument();
			if (contention != null) {
				target.append("contention", contention);
			}
			if (trimFactor != null) {
				target.append("trimFactor", trimFactor);
			}
			if (valueRange != null) {
				target.append("min", valueRange.getLowerBound().getValue().orElse((T) BsonNull.VALUE)).append("max",
						valueRange.getUpperBound().getValue().orElse((T) BsonNull.VALUE));
			}
			if (precision != null) {
				target.append("precision", precision);
			}
			if (sparsity != null) {
				target.append("sparsity", sparsity);
			}
			return target;
		}
	}
}