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

io.evitadb.api.requestResponse.EvitaRequest Maven / Gradle / Ivy

The newest version!
/*
 *
 *                         _ _        ____  ____
 *               _____   _(_) |_ __ _|  _ \| __ )
 *              / _ \ \ / / | __/ _` | | | |  _ \
 *             |  __/\ V /| | || (_| | |_| | |_) |
 *              \___| \_/ |_|\__\__,_|____/|____/
 *
 *   Copyright (c) 2023-2024
 *
 *   Licensed under the Business Source License, Version 1.1 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *   https://github.com/FgForrest/evitaDB/blob/master/LICENSE
 *
 *   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 io.evitadb.api.requestResponse;

import io.evitadb.api.EntityCollectionContract;
import io.evitadb.api.EvitaSessionContract;
import io.evitadb.api.exception.EntityCollectionRequiredException;
import io.evitadb.api.exception.UnexpectedResultException;
import io.evitadb.api.query.Query;
import io.evitadb.api.query.QueryUtils;
import io.evitadb.api.query.filter.EntityLocaleEquals;
import io.evitadb.api.query.filter.EntityPrimaryKeyInSet;
import io.evitadb.api.query.filter.FilterBy;
import io.evitadb.api.query.filter.HierarchyFilterConstraint;
import io.evitadb.api.query.filter.HierarchyWithin;
import io.evitadb.api.query.filter.PriceInCurrency;
import io.evitadb.api.query.filter.PriceInPriceLists;
import io.evitadb.api.query.filter.PriceValidIn;
import io.evitadb.api.query.head.Collection;
import io.evitadb.api.query.order.OrderBy;
import io.evitadb.api.query.require.*;
import io.evitadb.api.requestResponse.data.SealedEntity;
import io.evitadb.dataType.DataChunk;
import io.evitadb.dataType.PaginatedList;
import io.evitadb.dataType.StripList;
import io.evitadb.utils.ArrayUtils;
import io.evitadb.utils.Assert;
import lombok.Getter;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.AbstractMap.SimpleEntry;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

import static io.evitadb.api.query.QueryConstraints.collection;
import static io.evitadb.api.query.QueryConstraints.require;
import static java.util.Optional.ofNullable;

/**
 * Evita request serves as simple DTO that streamlines and caches access to the input {@link Query}.
 *
 * {@link EvitaRequest} is internal class (Evita accepts simple {@link Query} object -
 * see {@link EvitaSessionContract#query(Query, Class)}) that envelopes the input query. Evita request
 * can be used to implement methods that extract crucial information from the input query and cache those extracted
 * information to avoid paying parsing costs twice in single request.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
 * @see EvitaSessionContract#query(Query, Class)
 * @see EvitaResponse examples in super class
 */
public class EvitaRequest {
	public static final BiFunction, SealedEntity, ?> CONVERSION_NOT_SUPPORTED = (aClass, sealedEntity) -> {
		throw new UnsupportedOperationException();
	};
	private static final int[] EMPTY_INTS = new int[0];
	@Getter private final Query query;
	@Getter private final OffsetDateTime alignedNow;
	private final String entityType;
	private final Locale implicitLocale;
	private final Class expectedType;
	@Getter private final BiFunction, SealedEntity, ?> converter;
	private int[] primaryKeys;
	private boolean localeExamined;
	private Locale locale;
	private Boolean requiredLocales;
	private Set requiredLocaleSet;
	private QueryPriceMode queryPriceMode;
	private Boolean priceValidInTimeSet;
	private OffsetDateTime priceValidInTime;
	private Boolean requiresEntity;
	private Boolean requiresParent;
	private HierarchyContent parentContent;
	private EntityFetch entityRequirement;
	private Boolean entityAttributes;
	private Set entityAttributeSet;
	private Boolean entityAssociatedData;
	private Set entityAssociatedDataSet;
	private Boolean entityReference;
	private PriceContentMode entityPrices;
	private Boolean currencySet;
	private Currency currency;
	private Boolean requiresPriceLists;
	private String[] priceLists;
	private String[] additionalPriceLists;
	private Integer firstRecordOffset;
	private Map hierarchyWithin;
	private Boolean requiredWithinHierarchy;
	private Boolean requiresHierarchyStatistics;
	private Boolean requiresHierarchyParents;
	private Integer limit;
	private EvitaRequest.ResultForm resultForm;
	private Map facetGroupConjunction;
	private Map facetGroupDisjunction;
	private Map facetGroupNegation;
	private Boolean queryTelemetryRequested;
	private EnumSet debugModes;
	private Map entityFetchRequirements;
	private RequirementContext defaultReferenceRequirement;

	/**
	 * Parses the requirement context from the passed {@link ReferenceContent} and {@link AttributeContent}.
	 */
	@Nonnull
	private static RequirementContext getRequirementContext(
		@Nonnull ReferenceContent referenceContent,
		@Nullable AttributeContent attributeContent
	) {
		return new RequirementContext(
			referenceContent.getManagedReferencesBehaviour(),
			new AttributeRequest(
				attributeContent == null ? Collections.emptySet() : attributeContent.getAttributeNamesAsSet(),
				attributeContent != null
			),
			referenceContent.getEntityRequirement().orElse(null),
			referenceContent.getGroupEntityRequirement().orElse(null),
			referenceContent.getFilterBy().orElse(null),
			referenceContent.getOrderBy().orElse(null)
		);
	}

	public EvitaRequest(
		@Nonnull Query query,
		@Nonnull OffsetDateTime alignedNow,
		@Nonnull Class expectedType,
		@Nullable String entityTypeByExpectedType,
		@Nonnull BiFunction, SealedEntity, ?> converter
	) {
		final Collection header = query.getCollection();
		this.entityType = ofNullable(header).map(Collection::getEntityType).orElse(entityTypeByExpectedType);
		this.query = query;
		this.alignedNow = alignedNow;
		this.implicitLocale = null;
		this.expectedType = expectedType;
		this.converter = converter;
	}

	public EvitaRequest(@Nonnull EvitaRequest evitaRequest, @Nonnull Locale implicitLocale) {
		this.entityType = evitaRequest.entityType;
		this.query = evitaRequest.query;
		this.alignedNow = evitaRequest.alignedNow;
		this.implicitLocale = implicitLocale;
		this.primaryKeys = evitaRequest.primaryKeys;
		this.localeExamined = evitaRequest.localeExamined;
		this.locale = evitaRequest.locale;
		this.requiredLocales = evitaRequest.requiredLocales;
		this.requiredLocaleSet = evitaRequest.requiredLocaleSet;
		this.queryPriceMode = evitaRequest.queryPriceMode;
		this.priceValidInTimeSet = evitaRequest.priceValidInTimeSet;
		this.priceValidInTime = evitaRequest.priceValidInTime;
		this.entityAttributes = evitaRequest.entityAttributes;
		this.entityAttributeSet = evitaRequest.entityAttributeSet;
		this.entityAssociatedData = evitaRequest.entityAssociatedData;
		this.entityAssociatedDataSet = evitaRequest.entityAssociatedDataSet;
		this.entityReference = evitaRequest.entityReference;
		this.entityFetchRequirements = evitaRequest.entityFetchRequirements;
		this.defaultReferenceRequirement = evitaRequest.defaultReferenceRequirement;
		this.entityPrices = evitaRequest.entityPrices;
		this.currencySet = evitaRequest.currencySet;
		this.currency = evitaRequest.currency;
		this.requiresPriceLists = evitaRequest.requiresPriceLists;
		this.additionalPriceLists = evitaRequest.additionalPriceLists;
		this.priceLists = evitaRequest.priceLists;
		this.firstRecordOffset = evitaRequest.firstRecordOffset;
		this.hierarchyWithin = evitaRequest.hierarchyWithin;
		this.requiredWithinHierarchy = evitaRequest.requiredWithinHierarchy;
		this.requiresHierarchyStatistics = evitaRequest.requiresHierarchyStatistics;
		this.requiresHierarchyParents = evitaRequest.requiresHierarchyParents;
		this.limit = evitaRequest.limit;
		this.resultForm = evitaRequest.resultForm;
		this.facetGroupConjunction = evitaRequest.facetGroupConjunction;
		this.facetGroupDisjunction = evitaRequest.facetGroupDisjunction;
		this.facetGroupNegation = evitaRequest.facetGroupNegation;
		this.requiresEntity = evitaRequest.requiresEntity;
		this.requiresParent = evitaRequest.requiresParent;
		this.parentContent = evitaRequest.parentContent;
		this.entityRequirement = evitaRequest.entityRequirement;
		this.expectedType = evitaRequest.expectedType;
		this.converter = evitaRequest.converter;
	}

	public EvitaRequest(
		@Nonnull EvitaRequest evitaRequest,
		@Nonnull String entityType,
		@Nonnull EntityFetchRequire requirements
	) {

		this.requiresEntity = true;
		this.entityRequirement = new EntityFetch(requirements.getRequirements());
		this.entityType = entityType;
		this.query = Query.query(
			collection(entityType),
			evitaRequest.query.getFilterBy(),
			evitaRequest.query.getOrderBy(),
			require(this.entityRequirement)
		);
		this.alignedNow = evitaRequest.alignedNow;
		this.implicitLocale = evitaRequest.implicitLocale;
		this.primaryKeys = evitaRequest.primaryKeys;
		this.localeExamined = evitaRequest.localeExamined;
		this.locale = evitaRequest.locale;
		if (Arrays.stream(requirements.getRequirements()).anyMatch(it -> it instanceof DataInLocales)) {
			this.requiredLocales = null;
			this.requiredLocaleSet = null;
		} else {
			this.requiredLocales = evitaRequest.requiredLocales;
			this.requiredLocaleSet = evitaRequest.requiredLocaleSet;
		}
		this.queryPriceMode = evitaRequest.queryPriceMode;
		this.priceValidInTimeSet = evitaRequest.priceValidInTimeSet;
		this.priceValidInTime = evitaRequest.priceValidInTime;
		this.requiresParent = null;
		this.parentContent = null;
		this.entityAttributes = null;
		this.entityAttributeSet = null;
		this.entityAssociatedData = null;
		this.entityAssociatedDataSet = null;
		this.entityReference = null;
		this.entityFetchRequirements = null;
		this.defaultReferenceRequirement = null;
		this.entityPrices = null;
		this.currencySet = evitaRequest.currencySet;
		this.currency = evitaRequest.currency;
		this.requiresPriceLists = evitaRequest.requiresPriceLists;
		this.additionalPriceLists = evitaRequest.additionalPriceLists;
		this.priceLists = evitaRequest.priceLists;
		this.firstRecordOffset = evitaRequest.firstRecordOffset;
		this.hierarchyWithin = evitaRequest.hierarchyWithin;
		this.requiredWithinHierarchy = evitaRequest.requiredWithinHierarchy;
		this.requiresHierarchyStatistics = evitaRequest.requiresHierarchyStatistics;
		this.requiresHierarchyParents = evitaRequest.requiresHierarchyParents;
		this.limit = evitaRequest.limit;
		this.resultForm = evitaRequest.resultForm;
		this.facetGroupConjunction = evitaRequest.facetGroupConjunction;
		this.facetGroupDisjunction = evitaRequest.facetGroupDisjunction;
		this.facetGroupNegation = evitaRequest.facetGroupNegation;
		this.expectedType = evitaRequest.expectedType;
		this.converter = evitaRequest.converter;
	}

	public EvitaRequest(
		@Nonnull EvitaRequest evitaRequest,
		@Nonnull String entityType,
		@Nonnull FilterBy filterBy,
		@Nullable OrderBy orderBy,
		@Nullable Locale locale
	) {

		this.requiresEntity = true;
		this.entityRequirement = evitaRequest.entityRequirement;
		this.entityType = entityType;
		this.query = Query.query(
			collection(entityType),
			filterBy,
			orderBy,
			require(this.entityRequirement)
		);
		this.alignedNow = evitaRequest.getAlignedNow();
		this.implicitLocale = evitaRequest.getImplicitLocale();
		this.primaryKeys = null;
		this.queryPriceMode = evitaRequest.getQueryPriceMode();
		this.priceValidInTimeSet = true;
		this.priceValidInTime = evitaRequest.getRequiresPriceValidIn();
		this.currencySet = true;
		this.currency = evitaRequest.getRequiresCurrency();
		this.requiresPriceLists = evitaRequest.isRequiresPriceLists();
		this.priceLists = evitaRequest.getRequiresPriceLists();
		this.additionalPriceLists = evitaRequest.getFetchesAdditionalPriceLists();
		this.localeExamined = true;
		this.locale = locale == null ? evitaRequest.getLocale() : locale;
		this.requiredLocales = null;
		this.requiredLocaleSet = null;
		this.requiresParent = null;
		this.parentContent = null;
		this.entityAttributes = null;
		this.entityAttributeSet = null;
		this.entityAssociatedData = null;
		this.entityAssociatedDataSet = null;
		this.entityReference = null;
		this.entityFetchRequirements = null;
		this.defaultReferenceRequirement = null;
		this.entityPrices = null;
		this.firstRecordOffset = null;
		this.hierarchyWithin = null;
		this.requiredWithinHierarchy = null;
		this.requiresHierarchyStatistics = null;
		this.requiresHierarchyParents = null;
		this.limit = null;
		this.resultForm = null;
		this.facetGroupConjunction = null;
		this.facetGroupDisjunction = null;
		this.facetGroupNegation = null;
		this.expectedType = evitaRequest.expectedType;
		this.converter = evitaRequest.converter;
	}

	/**
	 * Returns true if query targets specific entity type.
	 */
	public boolean isEntityTypeRequested() {
		return entityType != null;
	}

	/**
	 * Returns type of the entity this query targets. Allows to choose proper {@link EntityCollectionContract}.
	 */
	@Nullable
	public String getEntityType() {
		return entityType;
	}

	/**
	 * Returns type of the entity this query targets. Allows to choose proper {@link EntityCollectionContract}.
	 */
	@Nonnull
	public String getEntityTypeOrThrowException(@Nonnull String purpose) {
		final Collection header = query.getCollection();
		return ofNullable(header)
			.map(Collection::getEntityType)
			.orElseThrow(() -> new EntityCollectionRequiredException(purpose));
	}

	/**
	 * Returns locale of the entity that is being requested.
	 */
	@Nullable
	public Locale getLocale() {
		if (!this.localeExamined) {
			this.localeExamined = true;
			this.locale = ofNullable(QueryUtils.findFilter(query, EntityLocaleEquals.class))
				.map(EntityLocaleEquals::getLocale)
				.orElse(null);
		}
		return this.locale;
	}

	/**
	 * Returns implicit locale that might be derived from the globally unique attribute if the entity is matched
	 * particularly by it.
	 */
	@Nullable
	public Locale getImplicitLocale() {
		return implicitLocale;
	}

	/**
	 * Returns locale of the entity that is being requested. If locale is not explicitly set in the query it falls back
	 * to {@link #getImplicitLocale()}.
	 */
	@Nullable
	public Locale getRequiredOrImplicitLocale() {
		return ofNullable(getLocale()).orElseGet(this::getImplicitLocale);
	}

	/**
	 * Returns set of locales if requirement {@link DataInLocales} is present in the query. If not it falls back to
	 * {@link EntityLocaleEquals} (check {@link DataInLocales} docs).
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	@Nullable
	public Set getRequiredLocales() {
		if (this.requiredLocales == null) {
			final EntityFetch entityFetch = QueryUtils.findRequire(query, EntityFetch.class, SeparateEntityContentRequireContainer.class);
			if (entityFetch == null) {
				this.requiredLocales = true;
				final Locale theLocale = getLocale();
				if (theLocale != null) {
					this.requiredLocaleSet = Set.of(theLocale);
				}
			} else {
				final DataInLocales dataRequirement = QueryUtils.findConstraint(
					entityFetch, DataInLocales.class, SeparateEntityContentRequireContainer.class
				);
				if (dataRequirement != null) {
					this.requiredLocaleSet = Arrays.stream(dataRequirement.getLocales())
						.filter(Objects::nonNull)
						.collect(Collectors.toSet());
				} else {
					final Locale theLocale = getLocale();
					if (theLocale != null) {
						this.requiredLocaleSet = Set.of(theLocale);
					}
				}
				this.requiredLocales = true;
			}
		}
		return this.requiredLocaleSet;
	}

	/**
	 * Returns query price mode of the current query.
	 */
	@Nonnull
	public QueryPriceMode getQueryPriceMode() {
		if (this.queryPriceMode == null) {
			this.queryPriceMode = ofNullable(QueryUtils.findRequire(query, PriceType.class))
				.map(PriceType::getQueryPriceMode)
				.orElse(QueryPriceMode.WITH_TAX);
		}
		return this.queryPriceMode;
	}

	/**
	 * Returns set of primary keys that are required by the query in {@link EntityPrimaryKeyInSet} query.
	 * If there is no such query empty array is returned in the result.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	@Nonnull
	public int[] getPrimaryKeys() {
		if (primaryKeys == null) {
			primaryKeys = ofNullable(QueryUtils.findFilter(query, EntityPrimaryKeyInSet.class, SeparateEntityContentRequireContainer.class))
				.map(EntityPrimaryKeyInSet::getPrimaryKeys)
				.orElse(EMPTY_INTS);
		}
		return primaryKeys;
	}

	/**
	 * Method will determine if at least entity body is required for main entities.
	 */
	public boolean isRequiresEntity() {
		if (this.requiresEntity == null) {
			final EntityFetch entityFetch = QueryUtils.findRequire(query, EntityFetch.class, SeparateEntityContentRequireContainer.class);
			this.requiresEntity = entityFetch != null;
			this.entityRequirement = entityFetch;
		}
		return this.requiresEntity;
	}

	/**
	 * Method will find all requirement specifying richness of main entities. The constraints inside
	 * {@link SeparateEntityContentRequireContainer} implementing of same type are ignored because they relate to the different entity context.
	 */
	@Nullable
	public EntityFetch getEntityRequirement() {
		if (this.requiresEntity == null) {
			isRequiresEntity();
		}
		return this.entityRequirement;
	}

	/**
	 * Method will determine if parent body is required for main entities.
	 */
	public boolean isRequiresParent() {
		if (this.requiresParent == null) {
			final EntityFetch entityFetch = getEntityRequirement();
			if (entityFetch == null) {
				this.parentContent = null;
				this.requiresParent = false;
			} else {
				this.parentContent = QueryUtils.findConstraint(entityFetch, HierarchyContent.class, SeparateEntityContentRequireContainer.class);
				this.requiresParent = this.parentContent != null;
			}
		}
		return this.requiresParent;
	}

	/**
	 * Method will find all requirement specifying richness of main entities. The constraints inside
	 * {@link SeparateEntityContentRequireContainer} implementing of same type are ignored because they relate to the
	 * different entity context.
	 */
	@Nullable
	public HierarchyContent getHierarchyContent() {
		if (this.requiresParent == null) {
			isRequiresParent();
		}
		return this.parentContent;
	}

	/**
	 * Returns TRUE if requirement {@link AttributeContent} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	public boolean isRequiresEntityAttributes() {
		if (entityAttributes == null) {
			final EntityFetch entityFetch = getEntityRequirement();
			if (entityFetch == null) {
				this.entityAttributes = false;
				this.entityAttributeSet = Collections.emptySet();
			} else {
				final AttributeContent requiresAttributeContent = QueryUtils.findConstraint(entityFetch, AttributeContent.class, SeparateEntityContentRequireContainer.class);
				this.entityAttributes = requiresAttributeContent != null;
				this.entityAttributeSet = requiresAttributeContent != null ?
					Arrays.stream(requiresAttributeContent.getAttributeNames()).collect(Collectors.toSet()) :
					Collections.emptySet();
			}
		}
		return entityAttributes;
	}

	/**
	 * Returns set of attribute names that were requested in the query. The set is empty if none is requested
	 * which means - all attributes is ought to be returned.
	 */
	@Nonnull
	public Set getEntityAttributeSet() {
		if (this.entityAttributeSet == null) {
			isRequiresEntityAttributes();
		}
		return this.entityAttributeSet;
	}

	/**
	 * Returns TRUE if requirement {@link AssociatedDataContent} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	public boolean isRequiresEntityAssociatedData() {
		if (entityAssociatedData == null) {
			final EntityFetch entityFetch = getEntityRequirement();
			if (entityFetch == null) {
				this.entityAssociatedData = false;
				this.entityAssociatedDataSet = Collections.emptySet();
			} else {
				final AssociatedDataContent requiresAssociatedDataContent = QueryUtils.findConstraint(entityFetch, AssociatedDataContent.class, SeparateEntityContentRequireContainer.class);
				this.entityAssociatedData = requiresAssociatedDataContent != null;
				this.entityAssociatedDataSet = requiresAssociatedDataContent != null ?
					Arrays.stream(requiresAssociatedDataContent.getAssociatedDataNames()).collect(Collectors.toSet()) :
					Collections.emptySet();
			}
		}
		return entityAssociatedData;
	}

	/**
	 * Returns set of associated data names that were requested in the query. The set is empty if none is requested
	 * which means - all associated data is ought to be returned.
	 */
	@Nonnull
	public Set getEntityAssociatedDataSet() {
		if (this.entityAssociatedDataSet == null) {
			isRequiresEntityAssociatedData();
		}
		return this.entityAssociatedDataSet;
	}

	/**
	 * Returns TRUE if requirement {@link ReferenceContent} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	public boolean isRequiresEntityReferences() {
		if (entityReference == null) {
			getReferenceEntityFetch();
		}
		return entityReference;
	}

	/**
	 * Returns {@link PriceContentMode} if requirement {@link PriceContent} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	@Nonnull
	public PriceContentMode getRequiresEntityPrices() {
		if (this.entityPrices == null) {
			final EntityFetch entityFetch = QueryUtils.findRequire(query, EntityFetch.class, SeparateEntityContentRequireContainer.class);
			if (entityFetch == null) {
				this.entityPrices = PriceContentMode.NONE;
				this.additionalPriceLists = PriceContent.EMPTY_PRICE_LISTS;
			} else {
				final Optional priceContentRequirement = ofNullable(QueryUtils.findConstraint(entityFetch, PriceContent.class, SeparateEntityContentRequireContainer.class));
				this.entityPrices = priceContentRequirement
					.map(PriceContent::getFetchMode)
					.orElse(PriceContentMode.NONE);
				this.additionalPriceLists = priceContentRequirement
					.map(PriceContent::getAdditionalPriceListsToFetch)
					.orElse(PriceContent.EMPTY_PRICE_LISTS);
			}
		}
		return this.entityPrices;
	}

	/**
	 * Returns array of price list ids if requirement {@link PriceContent} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	@Nonnull
	public String[] getFetchesAdditionalPriceLists() {
		if (this.additionalPriceLists == null) {
			getRequiresEntityPrices();
		}
		return this.additionalPriceLists;
	}

	/**
	 * Returns TRUE if any {@link PriceInPriceLists} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	public boolean isRequiresPriceLists() {
		if (this.requiresPriceLists == null) {
			final List priceInPriceLists = QueryUtils.findFilters(query, PriceInPriceLists.class);
			Assert.isTrue(
				priceInPriceLists.size() <= 1,
				"Query can not contain more than one price in price lists filter constraints!"
			);
			final Optional pricesInPriceList = priceInPriceLists.isEmpty() ?
				Optional.empty() : Optional.of(priceInPriceLists.get(0));
			this.priceLists = pricesInPriceList
				.map(PriceInPriceLists::getPriceLists)
				.orElse(new String[0]);
			this.requiresPriceLists = pricesInPriceList.isPresent();
		}
		return this.requiresPriceLists;
	}

	/**
	 * Returns array of price list ids if filter {@link PriceInPriceLists} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	@Nonnull
	public String[] getRequiresPriceLists() {
		if (this.priceLists == null) {
			isRequiresPriceLists();
		}
		return this.priceLists;
	}

	/**
	 * Returns set of price list ids if requirement {@link PriceInCurrency} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	@Nullable
	public Currency getRequiresCurrency() {
		if (this.currencySet == null) {
			final List currenciesFound = QueryUtils.findFilters(query, PriceInCurrency.class)
				.stream()
				.map(PriceInCurrency::getCurrency)
				.distinct()
				.toList();
			Assert.isTrue(
				currenciesFound.size() <= 1,
				"Query can not contain more than one currency filtering constraints!"
			);
			this.currency = currenciesFound.isEmpty() ? null : currenciesFound.get(0);
			this.currencySet = true;
		}
		return this.currency;
	}

	/**
	 * Returns price valid in datetime if requirement {@link io.evitadb.api.query.filter.PriceValidIn} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	@Nullable
	public OffsetDateTime getRequiresPriceValidIn() {
		if (this.priceValidInTimeSet == null) {
			final List validitySpan = QueryUtils.findFilters(query, PriceValidIn.class)
				.stream()
				.map(it -> it.getTheMoment(this::getAlignedNow))
				.distinct()
				.toList();
			Assert.isTrue(
				validitySpan.size() <= 1,
				"Query can not contain more than one price validity constraints!"
			);
			this.priceValidInTime = validitySpan.isEmpty() ? null : validitySpan.get(0);
			this.priceValidInTimeSet = true;
		}
		return this.priceValidInTime;
	}

	/**
	 * Returns filter by representing group entity primary keys of `referenceName` facets, that are requested to be
	 * joined by conjunction (AND) instead of default disjunction (OR).
	 */
	@Nonnull
	public Optional getFacetGroupConjunction(@Nonnull String referenceName) {
		if (this.facetGroupConjunction == null) {
			this.facetGroupConjunction = new HashMap<>();
			QueryUtils.findRequires(query, FacetGroupsConjunction.class)
				.forEach(it -> {
					final String reqReferenceName = it.getReferenceName();
					this.facetGroupConjunction.put(reqReferenceName, new FacetFilterBy(it.getFacetGroups().orElse(null)));
				});
		}
		return ofNullable(this.facetGroupConjunction.get(referenceName));
	}

	/**
	 * Returns filter by representing group entity primary keys of `referenceName` facets, that are requested to be
	 * joined with other facet groups by disjunction (OR) instead of default conjunction (AND).
	 */
	@Nonnull
	public Optional getFacetGroupDisjunction(@Nonnull String referenceName) {
		if (this.facetGroupDisjunction == null) {
			this.facetGroupDisjunction = new HashMap<>();
			QueryUtils.findRequires(query, FacetGroupsDisjunction.class)
				.forEach(it -> {
					final String reqReferenceName = it.getReferenceName();
					this.facetGroupDisjunction.put(reqReferenceName, new FacetFilterBy(it.getFacetGroups().orElse(null)));
				});
		}
		return ofNullable(this.facetGroupDisjunction.get(referenceName));
	}

	/**
	 * Returns filter by representing group entity primary keys of `referenceName` facets, that are requested to be
	 * joined by negation (AND NOT) instead of default disjunction (OR).
	 */
	@Nonnull
	public Optional getFacetGroupNegation(@Nonnull String referenceName) {
		if (this.facetGroupNegation == null) {
			this.facetGroupNegation = new HashMap<>();
			QueryUtils.findRequires(query, FacetGroupsNegation.class)
				.forEach(it -> {
					final String reqReferenceName = it.getReferenceName();
					this.facetGroupNegation.put(reqReferenceName, new FacetFilterBy(it.getFacetGroups().orElse(null)));
				});
		}
		return ofNullable(this.facetGroupNegation.get(referenceName));
	}

	/**
	 * Returns TRUE if requirement {@link QueryTelemetry} is present in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	public boolean isQueryTelemetryRequested() {
		if (queryTelemetryRequested == null) {
			this.queryTelemetryRequested = QueryUtils.findRequire(query, QueryTelemetry.class) != null;
		}
		return queryTelemetryRequested;
	}

	/**
	 * Returns true if passed {@link DebugMode} is enabled in the query.
	 * Accessor method cache the found result so that consecutive calls of this method are pretty fast.
	 */
	public boolean isDebugModeEnabled(@Nonnull DebugMode debugMode) {
		if (debugModes == null) {
			this.debugModes = ofNullable(QueryUtils.findRequire(query, Debug.class))
				.map(Debug::getDebugMode)
				.orElseGet(() -> EnumSet.noneOf(DebugMode.class));
		}
		return debugModes.contains(debugMode);
	}

	/**
	 * Returns count of records required in the result (i.e. number of records on a single page).
	 */
	public int getLimit() {
		if (limit == null) {
			initPagination();
		}
		return limit;
	}

	/**
	 * Returns requested record offset of the records required in the result.
	 */
	public int getFirstRecordOffset() {
		if (firstRecordOffset == null) {
			initPagination();
		}
		return firstRecordOffset;
	}

	/**
	 * Returns requested record offset of the records required in the result.
	 * Offset is automatically reset to zero if requested offset exceeds the total available record count.
	 */
	public int getFirstRecordOffset(int totalRecordCount) {
		if (firstRecordOffset == null) {
			initPagination();
		}
		return firstRecordOffset >= totalRecordCount ? 0 : firstRecordOffset;
	}

	/**
	 * Returns default requirements for reference content.
	 */
	@Nullable
	public RequirementContext getDefaultReferenceRequirement() {
		getReferenceEntityFetch();
		return defaultReferenceRequirement;
	}

	/**
	 * Returns requested referenced entity requirements from the input query.
	 * Allows traversing through the object relational graph in unlimited depth.
	 */
	@Nonnull
	public Map getReferenceEntityFetch() {
		if (entityFetchRequirements == null) {
			entityFetchRequirements = ofNullable(getEntityRequirement())
				.map(
					entityRequirement -> {
						final List referenceContent = QueryUtils.findConstraints(entityRequirement, ReferenceContent.class, SeparateEntityContentRequireContainer.class);
						this.entityReference = !referenceContent.isEmpty();
						this.defaultReferenceRequirement = referenceContent
							.stream()
							.filter(it -> ArrayUtils.isEmpty(it.getReferenceNames()))
							.map(it -> getRequirementContext(it, it.getAttributeContent().orElse(null)))
							.findFirst()
							.orElse(null);

						return referenceContent
							.stream()
							.flatMap(it ->
								Arrays
									.stream(it.getReferenceNames())
									.map(
										entityType -> new SimpleEntry<>(
											entityType,
											getRequirementContext(it, it.getAttributeContent().orElse(null))
										)
									)
							)
							.collect(
								Collectors.toMap(
									SimpleEntry::getKey,
									SimpleEntry::getValue
								)
							);
					}
				).orElseGet(() -> {
					this.entityReference = false;
					return Collections.emptyMap();
				});
		}
		return entityFetchRequirements;
	}

	/**
	 * Returns {@link HierarchyWithin} query
	 */
	@Nullable
	public HierarchyFilterConstraint getHierarchyWithin(@Nullable String referenceName) {
		if (requiredWithinHierarchy == null) {
			if (query.getFilterBy() == null) {
				hierarchyWithin = Collections.emptyMap();
			} else {
				hierarchyWithin = new HashMap<>();
				QueryUtils.findConstraints(
						query.getFilterBy(),
						HierarchyFilterConstraint.class
					)
					.forEach(it -> hierarchyWithin.put(it.getReferenceName().orElse(null), it));
			}
			requiredWithinHierarchy = true;
		}
		return hierarchyWithin.get(referenceName);
	}

	/**
	 * Method creates requested implementation of {@link DataChunk} with results.
	 */
	@Nonnull
	public  DataChunk createDataChunk(int totalRecordCount, @Nonnull List data) {
		if (firstRecordOffset == null) {
			initPagination();
		}

		if (!data.isEmpty()) {
			if (!expectedType.isInstance(data.get(0))) {
				if (data.get(0) instanceof SealedEntity) {
					//noinspection unchecked
					data = (List) data.stream()
						.map(SealedEntity.class::cast)
						.map(it -> converter.apply(expectedType, it))
						.toList();
				} else {
					throw new UnexpectedResultException(expectedType, data.get(0).getClass());
				}
			}
		}

		return switch (resultForm) {
			case PAGINATED_LIST ->
				new PaginatedList<>(limit == 0 ? 1 : (firstRecordOffset + limit) / limit, limit, totalRecordCount, data);
			case STRIP_LIST -> new StripList<>(firstRecordOffset, limit, totalRecordCount, data);
		};
	}

	/**
	 * Method creates copy of this request with changed `entityType` and entity `requirements`. The copy will share
	 * already resolved and memoized values of this request except those that relate to the changed entity type and
	 * requirements.
	 */
	@Nonnull
	public EvitaRequest deriveCopyWith(@Nonnull String entityType, @Nonnull EntityFetchRequire requirements) {
		return new EvitaRequest(
			this,
			entityType, requirements
		);
	}

	/**
	 * Method creates copy of this request with changed `entityType` and `filterConstraint`. The copy will share
	 * already resolved and memoized values of this request except those that relate to the changed entity type and
	 * the filtering constraints.
	 */
	@Nonnull
	public EvitaRequest deriveCopyWith(
		@Nonnull String entityType,
		@Nullable FilterBy filterConstraint,
		@Nullable OrderBy orderConstraint,
		@Nullable Locale locale
	) {
		return new EvitaRequest(
			this,
			entityType, filterConstraint, orderConstraint, locale
		);
	}

	/**
	 * Internal method that consults input query and initializes pagination information.
	 * If there is no pagination in the input query, first page with size of 20 records is used as default.
	 */
	private void initPagination() {
		final Optional page = ofNullable(QueryUtils.findRequire(query, Page.class));
		final Optional strip = ofNullable(QueryUtils.findRequire(query, Strip.class));
		if (page.isPresent()) {
			limit = page.get().getPageSize();
			firstRecordOffset = PaginatedList.getFirstItemNumberForPage(page.get().getPageNumber(), limit);
			resultForm = EvitaRequest.ResultForm.PAGINATED_LIST;
		} else if (strip.isPresent()) {
			limit = strip.get().getLimit();
			firstRecordOffset = strip.get().getOffset();
			resultForm = EvitaRequest.ResultForm.STRIP_LIST;
		} else {
			limit = 20;
			firstRecordOffset = 0;
			resultForm = EvitaRequest.ResultForm.PAGINATED_LIST;
		}
	}

	private enum ResultForm {
		PAGINATED_LIST, STRIP_LIST
	}

	/**
	 * Simple DTO that allows collection of {@link ReferenceContent} inner constraints related to fetching the entity
	 * and group entity for fast access in this evita request instance.
	 *
	 * @param managedReferencesBehaviour controls behaviour of excluding missing managed references
	 * @param attributeRequest           requested attributes for the entity reference
	 * @param entityFetch                requirements related to fetching related entity
	 * @param entityGroupFetch           requirements related to fetching related entity group
	 * @param filterBy                   filtering constraints for entities
	 * @param orderBy                    ordering constraints for entities
	 */
	public record RequirementContext(
		@Nonnull ManagedReferencesBehaviour managedReferencesBehaviour,
		@Nonnull AttributeRequest attributeRequest,
		@Nullable EntityFetch entityFetch,
		@Nullable EntityGroupFetch entityGroupFetch,
		@Nullable FilterBy filterBy,
		@Nullable OrderBy orderBy
	) {

		/**
		 * Returns true if the settings require initialization of referenced entities.
		 * @return true if the settings require initialization of referenced entities
		 */
		public boolean requiresInit() {
			return managedReferencesBehaviour != ManagedReferencesBehaviour.ANY ||
				entityFetch != null || entityGroupFetch != null || filterBy != null || orderBy != null;
		}

	}

	/**
	 * Attribute request DTO contains information about all attribute names that has been requested for the particular
	 * reference.
	 *
	 * @param attributeSet             Contains information about all attribute names that has been fetched / requested for the entity.
	 * @param requiresEntityAttributes Contains true if any of the attributes of the entity has been fetched / requested.
	 */
	public record AttributeRequest(
		@Nonnull Set attributeSet,
		@Getter boolean requiresEntityAttributes
	) implements Serializable {
		/**
		 * Represents a request for no attributes to be fetched.
		 */
		public static final AttributeRequest EMPTY = new AttributeRequest(Collections.emptySet(), false);
		/**
		 * Represents a request for all attributes to be fetched.
		 */
		public static final AttributeRequest ALL = new AttributeRequest(Collections.emptySet(), true);
	}

	/**
	 * Wraps the information whether the facet group was altered by a refinement constraint and if so, whether
	 * filterBy constraint was provided or not.
	 *
	 * @param filterBy filterBy constraint that was provided by the refinement constraint
	 */
	public record FacetFilterBy(
		@Nullable FilterBy filterBy
	) {

		public boolean isFilterDefined() {
			return filterBy != null;
		}

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy