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

io.evitadb.api.requestResponse.schema.dto.ReflectedReferenceSchema Maven / Gradle / Ivy

The newest version!
/*
 *
 *                         _ _        ____  ____
 *               _____   _(_) |_ __ _|  _ \| __ )
 *              / _ \ \ / / | __/ _` | | | |  _ \
 *             |  __/\ V /| | || (_| | |_| | |_) |
 *              \___| \_/ |_|\__\__,_|____/|____/
 *
 *   Copyright (c) 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.schema.dto;


import io.evitadb.api.exception.InvalidSchemaMutationException;
import io.evitadb.api.exception.SchemaAlteringException;
import io.evitadb.api.requestResponse.schema.AttributeSchemaContract;
import io.evitadb.api.requestResponse.schema.Cardinality;
import io.evitadb.api.requestResponse.schema.CatalogSchemaContract;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.api.requestResponse.schema.ReferenceSchemaContract;
import io.evitadb.api.requestResponse.schema.ReflectedReferenceSchemaContract;
import io.evitadb.api.requestResponse.schema.SortableAttributeCompoundSchemaContract;
import io.evitadb.dataType.ClassifierType;
import io.evitadb.dataType.Predecessor;
import io.evitadb.dataType.ReferencedEntityPredecessor;
import io.evitadb.exception.GenericEvitaInternalError;
import io.evitadb.utils.ArrayUtils;
import io.evitadb.utils.Assert;
import io.evitadb.utils.ClassifierUtils;
import io.evitadb.utils.CollectionUtils;
import io.evitadb.utils.NamingConvention;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
import java.io.Serial;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Internal implementation of {@link ReflectedReferenceSchemaContract}.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2024
 * @see ReflectedReferenceSchemaContract
 */
@Immutable
@ThreadSafe
public final class ReflectedReferenceSchema extends ReferenceSchema implements ReflectedReferenceSchemaContract {
	@Serial private static final long serialVersionUID = 4857683151308476440L;

	/**
	 * Contains name of the original reference of the {@link #getReferencedEntityType()} this reference reflects.
	 */
	private final String reflectedReferenceName;
	/**
	 * Contains the original reference this one reflects. Might be null for instances created in builders. If the schema
	 * instance is present on the server side, it may by null only if referenced entity doesn't (yet) have the reflected
	 * reference by that name.
	 */
	@Nullable
	private final ReferenceSchemaContract reflectedReference;
	/**
	 * Contains TRUE if the description of the reflected reference is inherited from the target reference.
	 */
	private final boolean descriptionInherited;
	/**
	 * Contains TRUE if the deprecated flag of the reflected reference is inherited from the target reference.
	 */
	private final boolean deprecatedInherited;
	/**
	 * Contains TRUE if the cardinality of the reflected reference is inherited from the target reference.
	 */
	private final boolean cardinalityInherited;
	/**
	 * Contains TRUE if the faceted flag of the reflected reference is inherited from the target reference.
	 */
	private final boolean facetedInherited;
	/**
	 * Contains TRUE if the attributes of the reflected reference are inherited from the target reference.
	 */
	@Nonnull
	private final AttributeInheritanceBehavior attributesInheritanceBehavior;
	/**
	 * Contains the names of the attributes that are explicitly inherited / excluded from inheritance
	 * based on {@link #attributesInheritanceBehavior} value.
	 */
	@Nonnull
	private final String[] attributeInheritanceFilter;
	/**
	 * Set of inherited attribute from the original reference that is reflected by this one. When the original reference
	 * is not present, this set is empty.
	 */
	@Nonnull
	private final Set inheritedAttributes;

	/**
	 * This method is for internal purposes only. It could be used for reconstruction of ReferenceSchema from
	 * different package than current, but still internal code of the Evita ecosystems.
	 *
	 * Do not use this method from in the client code!
	 */
	@Nonnull
	public static ReflectedReferenceSchema _internalBuild(
		@Nonnull String name,
		@Nonnull String entityType,
		@Nonnull String reflectedReferenceName
	) {
		ClassifierUtils.validateClassifierFormat(ClassifierType.ENTITY, entityType);

		return new ReflectedReferenceSchema(
			name, NamingConvention.generate(name),
			null, null, null,
			entityType,
			reflectedReferenceName,
			null,
			Collections.emptyMap(),
			Collections.emptyMap(),
			AttributeInheritanceBehavior.INHERIT_ONLY_SPECIFIED,
			new String[0],
			null
		);
	}

	/**
	 * This method is for internal purposes only. It could be used for reconstruction of ReferenceSchema from
	 * different package than current, but still internal code of the Evita ecosystems.
	 *
	 * Do not use this method from in the client code!
	 */
	@Nonnull
	public static ReflectedReferenceSchema _internalBuild(
		@Nonnull String name,
		@Nullable String description,
		@Nullable String deprecationNotice,
		@Nonnull String entityType,
		@Nonnull String reflectedReferenceName,
		@Nullable Cardinality cardinality,
		@Nullable Boolean faceted,
		@Nonnull Map attributes,
		@Nonnull Map sortableAttributeCompounds,
		@Nonnull AttributeInheritanceBehavior attributesInherited,
		@Nullable String[] attributesExcludedFromInheritance
	) {
		ClassifierUtils.validateClassifierFormat(ClassifierType.ENTITY, entityType);
		return new ReflectedReferenceSchema(
			name, NamingConvention.generate(name),
			description, deprecationNotice, cardinality,
			entityType,
			reflectedReferenceName,
			faceted,
			attributes,
			sortableAttributeCompounds,
			attributesInherited,
			attributesExcludedFromInheritance == null ? new String[0] : attributesExcludedFromInheritance,
			null
		);
	}

	/**
	 * This method is for internal purposes only. It could be used for reconstruction of ReferenceSchema from
	 * different package than current, but still internal code of the Evita ecosystems.
	 *
	 * Do not use this method from in the client code!
	 */
	@Nonnull
	public static ReflectedReferenceSchema _internalBuild(
		@Nonnull String name,
		@Nonnull Map nameVariants,
		@Nullable String description,
		@Nullable String deprecationNotice,
		@Nonnull String entityType,
		@Nonnull String reflectedReferenceName,
		@Nullable Cardinality cardinality,
		@Nullable Boolean faceted,
		@Nonnull Map attributes,
		@Nonnull Map sortableAttributeCompounds,
		@Nonnull AttributeInheritanceBehavior attributesInherited,
		@Nullable String[] attributesExcludedFromInheritance
	) {
		ClassifierUtils.validateClassifierFormat(ClassifierType.ENTITY, entityType);
		return new ReflectedReferenceSchema(
			name, nameVariants,
			description, deprecationNotice, cardinality,
			entityType,
			reflectedReferenceName,
			faceted,
			attributes,
			sortableAttributeCompounds,
			attributesInherited,
			attributesExcludedFromInheritance == null ? new String[0] : attributesExcludedFromInheritance,
			null
		);
	}

	/**
	 * This method is for internal purposes only. It could be used for reconstruction of ReferenceSchema from
	 * different package than current, but still internal code of the Evita ecosystems.
	 *
	 * Do not use this method from in the client code!
	 */
	@Nonnull
	public static ReflectedReferenceSchema _internalBuild(
		@Nonnull String name,
		@Nonnull Map nameVariants,
		@Nullable String description,
		@Nullable String deprecationNotice,
		@Nonnull String entityType,
		@Nonnull Map entityTypeVariants,
		@Nonnull String referencedGroupType,
		@Nonnull Map groupTypeVariants,
		boolean referencedGroupManaged,
		@Nonnull String reflectedReferenceName,
		@Nullable Cardinality cardinality,
		@Nullable Boolean faceted,
		@Nonnull Map attributes,
		@Nonnull Map sortableAttributeCompounds,
		boolean descriptionInherited,
		boolean deprecatedInherited,
		boolean cardinalityInherited,
		boolean facetedInherited,
		@Nonnull AttributeInheritanceBehavior attributesInherited,
		@Nullable String[] attributesExcludedFromInheritance,
		@Nullable ReferenceSchemaContract reflectedReference
	) {
		ClassifierUtils.validateClassifierFormat(ClassifierType.ENTITY, entityType);
		return new ReflectedReferenceSchema(
			name, nameVariants,
			description, deprecationNotice, cardinality,
			entityType, entityTypeVariants,
			referencedGroupType, groupTypeVariants, referencedGroupManaged,
			reflectedReferenceName,
			faceted,
			attributes,
			sortableAttributeCompounds,
			descriptionInherited,
			deprecatedInherited,
			cardinalityInherited,
			facetedInherited,
			attributesInherited,
			attributesExcludedFromInheritance == null ? new String[0] : attributesExcludedFromInheritance,
			reflectedReference
		);
	}

	/**
	 * Returns new map that contains all the attributes of the reference and the reflected reference except the ones
	 * that are excluded from inheritance.
	 *
	 * @param attributes                    the attributes of the reference
	 * @param reflectedAttributes           the attributes of the reflected reference (inherited ones)
	 * @param attributesInheritanceBehavior the inheritance mode to use
	 * @param attributeInheritanceFilter    the attributes inherited / excluded from inheritance
	 * @param                            the type of the attributes
	 * @return the union of the attributes
	 */
	@Nonnull
	private static  Map union(
		@Nonnull Map attributes,
		@Nonnull Map reflectedAttributes,
		@Nonnull AttributeInheritanceBehavior attributesInheritanceBehavior,
		@Nonnull String[] attributeInheritanceFilter
	) {
		final Set filteredAttributes = Set.of(attributeInheritanceFilter);
		final HashMap result = new HashMap<>(attributes);
		final Predicate attributeFilter = attributesInheritanceBehavior == AttributeInheritanceBehavior.INHERIT_ONLY_SPECIFIED ?
			filteredAttributes::contains : attribute -> !filteredAttributes.contains(attribute);
		for (Entry reflectedEntry : reflectedAttributes.entrySet()) {
			if (attributeFilter.test(reflectedEntry.getKey())) {
				Assert.isPremiseValid(
					!result.containsKey(reflectedEntry.getKey()),
					"Attribute `" + reflectedEntry.getKey() + "` is inherited from the reflected reference, " +
						"but it is already defined!"
				);
				result.put(reflectedEntry.getKey(), reflectedEntry.getValue());
			}
		}
		return result;
	}

	/**
	 * Returns a new map that contains the attributes from the "unionAttributes" map that satisfy the given predicate
	 * "isOriginalPredicate". This method is used to inverse the effect of {@link #union(Map, Map, AttributeInheritanceBehavior, String[])} method.
	 *
	 * @param unionAttributes     the map containing the attributes
	 * @param isOriginalPredicate the predicate to filter the attributes
	 * @param                  the type of the attributes
	 * @return a new map with the intersected attributes
	 */
	@Nonnull
	private static  Map intersect(@Nonnull Map unionAttributes, @Nonnull Predicate isOriginalPredicate) {
		return unionAttributes
			.entrySet()
			.stream()
			.filter(entry -> !isOriginalPredicate.test(entry.getKey()))
			.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
	}

	/**
	 * This method processes the given map of attribute schema contracts and returns
	 * a new map where the types of necessary attributes are inverted based on specific conditions.
	 *
	 * @param inputSchemas A map containing attribute schema contracts identified by a string key.
	 * @return A map with the types of the necessary attributes inverted, or the original map if no inversion is needed.
	 */
	@Nonnull
	private static Map invertNecessaryAttributeTypes(
		@Nonnull Map inputSchemas
	) {
		// we optimize for a scenario where no attribute schema needs inverted type
		Map invertedTypes = null;
		for (Entry entry : inputSchemas.entrySet()) {
			if (Predecessor.class.equals(entry.getValue().getPlainType()) ||
				ReferencedEntityPredecessor.class.equals(entry.getValue().getPlainType())) {
				invertedTypes = invertedTypes == null ? CollectionUtils.createHashMap(inputSchemas.size()) : invertedTypes;
				invertedTypes.put(
					entry.getKey(),
					((AttributeSchema) entry.getValue()).withInvertedType()
				);
			}
		}
		if (invertedTypes != null) {
			// and we pay for it by second iteration
			for (Entry entry : inputSchemas.entrySet()) {
				if (!invertedTypes.containsKey(entry.getKey())) {
					invertedTypes.put(entry.getKey(), entry.getValue());
				}
			}
			return invertedTypes;
		} else {
			return inputSchemas;
		}
	}

	public ReflectedReferenceSchema(
		@Nonnull String name,
		@Nonnull Map nameVariants,
		@Nullable String description,
		@Nullable String deprecationNotice,
		@Nullable Cardinality cardinality,
		@Nonnull String referencedEntityType,
		@Nonnull String reflectedReferenceName,
		@Nullable Boolean faceted,
		@Nonnull Map attributes,
		@Nonnull Map sortableAttributeCompounds,
		@Nonnull AttributeInheritanceBehavior attributesInheritanceBehavior,
		@Nullable String[] attributeInheritanceFilter,
		@Nullable ReferenceSchemaContract reflectedReference
	) {
		super(
			name, nameVariants,
			reflectedReference != null && description == null ? reflectedReference.getDescription() : description,
			reflectedReference != null && deprecationNotice == null ? reflectedReference.getDeprecationNotice() : deprecationNotice,
			reflectedReference != null && cardinality == null ? reflectedReference.getCardinality() : cardinality,
			referencedEntityType,
			Map.of(), true,
			reflectedReference != null ? reflectedReference.getReferencedGroupType() : null,
			Map.of(),
			reflectedReference != null && reflectedReference.isReferencedGroupTypeManaged(),
			true,
			faceted == null ? reflectedReference != null && reflectedReference.isFaceted() : faceted,
			reflectedReference == null ?
				attributes :
				union(
					attributes,
					invertNecessaryAttributeTypes(reflectedReference.getAttributes()),
					attributesInheritanceBehavior,
					attributeInheritanceFilter
				),
			reflectedReference == null ?
				sortableAttributeCompounds :
				union(
					sortableAttributeCompounds,
					reflectedReference.getSortableAttributeCompounds(),
					attributesInheritanceBehavior,
					attributeInheritanceFilter
				)
		);
		Assert.isTrue(
			reflectedReference == null || reflectedReference.getName().equals(reflectedReferenceName),
			() -> "Reflected reference name `" + referencedEntityType + "` must have the same name as the target reference (`" + reflectedReference.getName() + "`)!"
		);
		Assert.isTrue(
			reflectedReference == null || reflectedReference.isReferencedEntityTypeManaged(),
			() -> "Reflected reference name `" + referencedEntityType + "` must refer to a managed entity type!"
		);
		this.reflectedReferenceName = reflectedReferenceName;
		this.reflectedReference = reflectedReference;
		this.descriptionInherited = description == null;
		this.deprecatedInherited = deprecationNotice == null;
		this.cardinalityInherited = cardinality == null;
		Assert.isTrue(
			this.cardinalityInherited || cardinality != null,
			"Cardinality must be either inherited or specified explicitly!"
		);
		this.facetedInherited = faceted == null;
		Assert.isTrue(
			this.facetedInherited || faceted != null,
			"Faceted must be either inherited or specified explicitly!"
		);
		this.attributesInheritanceBehavior = attributesInheritanceBehavior;
		this.attributeInheritanceFilter = attributeInheritanceFilter == null ? new String[0] : attributeInheritanceFilter;
		if (this.reflectedReference == null) {
			this.inheritedAttributes = Collections.emptySet();
		} else {
			switch (this.attributesInheritanceBehavior) {
				case INHERIT_ONLY_SPECIFIED -> {
					this.inheritedAttributes = Arrays.stream(this.attributeInheritanceFilter).collect(Collectors.toCollection(HashSet::new));
					this.inheritedAttributes.retainAll(this.reflectedReference.getAttributes().keySet());
				}
				case INHERIT_ALL_EXCEPT -> {
					this.inheritedAttributes = new HashSet<>(this.reflectedReference.getAttributes().keySet());
					Arrays.asList(this.attributeInheritanceFilter).forEach(this.inheritedAttributes::remove);
				}
				default -> throw new GenericEvitaInternalError(
					"Unsupported attribute inheritance behavior: " + this.attributesInheritanceBehavior
				);
			}
		}
	}

	public ReflectedReferenceSchema(
		@Nonnull String name,
		@Nonnull Map nameVariants,
		@Nullable String description,
		@Nullable String deprecationNotice,
		@Nullable Cardinality cardinality,
		@Nonnull String referencedEntityType,
		@Nonnull Map entityTypeVariants,
		@Nonnull String referencedGroupType,
		@Nullable Map groupTypeVariants,
		boolean referencedGroupManaged,
		@Nonnull String reflectedReferenceName,
		@Nullable Boolean faceted,
		@Nonnull Map attributes,
		@Nonnull Map sortableAttributeCompounds,
		boolean descriptionInherited,
		boolean deprecatedInherited,
		boolean cardinalityInherited,
		boolean facetedInherited,
		@Nonnull AttributeInheritanceBehavior attributesInheritanceBehavior,
		@Nullable String[] attributeInheritanceFilter,
		@Nullable ReferenceSchemaContract reflectedReference
	) {
		super(
			name, nameVariants,
			description,
			deprecationNotice,
			cardinality,
			referencedEntityType,
			entityTypeVariants,
			true,
			referencedGroupType,
			groupTypeVariants == null ? Map.of() : groupTypeVariants,
			referencedGroupManaged,
			true,
			faceted,
			attributes,
			sortableAttributeCompounds
		);
		this.reflectedReferenceName = reflectedReferenceName;
		this.reflectedReference = reflectedReference;
		this.descriptionInherited = descriptionInherited;
		this.deprecatedInherited = deprecatedInherited;
		this.cardinalityInherited = cardinalityInherited;
		this.facetedInherited = facetedInherited;
		this.attributesInheritanceBehavior = attributesInheritanceBehavior;
		this.attributeInheritanceFilter = attributeInheritanceFilter == null ? new String[0] : attributeInheritanceFilter;
		if (this.reflectedReference == null) {
			this.inheritedAttributes = Collections.emptySet();
		} else {
			switch (this.attributesInheritanceBehavior) {
				case INHERIT_ONLY_SPECIFIED -> {
					this.inheritedAttributes = Arrays.stream(this.attributeInheritanceFilter).collect(Collectors.toCollection(HashSet::new));
					this.inheritedAttributes.retainAll(this.reflectedReference.getAttributes().keySet());
				}
				case INHERIT_ALL_EXCEPT -> {
					this.inheritedAttributes = new HashSet<>(this.reflectedReference.getAttributes().keySet());
					Arrays.asList(this.attributeInheritanceFilter).forEach(this.inheritedAttributes::remove);
				}
				default -> throw new GenericEvitaInternalError(
					"Unsupported attribute inheritance behavior: " + this.attributesInheritanceBehavior
				);
			}
		}
	}

	@Nonnull
	@Override
	public String getReflectedReferenceName() {
		return this.reflectedReferenceName;
	}

	@Override
	public boolean isDescriptionInherited() {
		return this.descriptionInherited;
	}

	@Override
	public boolean isDeprecatedInherited() {
		return this.deprecatedInherited;
	}

	@Override
	public boolean isCardinalityInherited() {
		return this.cardinalityInherited;
	}

	@Override
	public boolean isFacetedInherited() {
		return this.facetedInherited;
	}

	@Nonnull
	@Override
	public AttributeInheritanceBehavior getAttributesInheritanceBehavior() {
		return this.attributesInheritanceBehavior;
	}

	@Nonnull
	@Override
	public String[] getAttributeInheritanceFilter() {
		return this.attributeInheritanceFilter;
	}

	@Override
	public boolean isReflectedReferenceAvailable() {
		return this.reflectedReference != null;
	}

	@Nullable
	@Override
	public String getDescription() {
		Assert.isTrue(
			!this.descriptionInherited || this.reflectedReference != null,
			"Description of the reflected reference is inherited from the target reference, but the reflected reference is not available!"
		);
		return super.getDescription();
	}

	@Nullable
	@Override
	public String getDeprecationNotice() {
		Assert.isTrue(
			!this.deprecatedInherited || this.reflectedReference != null,
			"Deprecation notice of the reflected reference is inherited from the target reference, but the reflected reference is not available!"
		);
		return super.getDeprecationNotice();
	}

	@Nonnull
	@Override
	public Cardinality getCardinality() {
		Assert.isTrue(
			!this.cardinalityInherited || this.reflectedReference != null,
			"Cardinality of the reflected reference is inherited from the target reference, but the reflected reference is not available!"
		);
		return super.getCardinality();
	}

	@Nullable
	@Override
	public String getReferencedGroupType() {
		Assert.isTrue(
			this.reflectedReference != null,
			"Reflected reference must be available to return a referenced group type!"
		);
		return this.reflectedReference.getReferencedGroupType();
	}

	@Override
	public boolean isReferencedGroupTypeManaged() {
		Assert.isTrue(
			this.reflectedReference != null,
			"Reflected reference must be available to return if the referenced group type is managed!"
		);
		return this.reflectedReference.isReferencedGroupTypeManaged();
	}

	@Override
	public boolean isIndexed() {
		// reflected references are required to be indexed
		return true;
	}

	@Override
	public boolean isFaceted() {
		Assert.isTrue(
			!this.facetedInherited || this.reflectedReference != null,
			"Faceted property of the reflected reference is inherited from the target reference, but the reflected reference is not available!"
		);
		return super.isFaceted();
	}

	@Nonnull
	@Override
	public Map getNonNullableOrDefaultValueAttributes() {
		assertAttributes();
		return super.getNonNullableOrDefaultValueAttributes();
	}

	@Nonnull
	@Override
	public Optional getAttribute(@Nonnull String attributeName) {
		assertAttributes();
		return super.getAttribute(attributeName);
	}

	@Nonnull
	@Override
	public Optional getAttributeByName(@Nonnull String attributeName, @Nonnull NamingConvention namingConvention) {
		assertAttributes();
		return super.getAttributeByName(attributeName, namingConvention);
	}

	@Nonnull
	@Override
	public Map getAttributes() {
		assertAttributes();
		return super.getAttributes();
	}

	@Nonnull
	@Override
	public Map getSortableAttributeCompounds() {
		assertAttributes();
		return super.getSortableAttributeCompounds();
	}

	@Nonnull
	@Override
	public Optional getSortableAttributeCompound(@Nonnull String name) {
		assertAttributes();
		return super.getSortableAttributeCompound(name);
	}

	@Nonnull
	@Override
	public Optional getSortableAttributeCompoundByName(@Nonnull String name, @Nonnull NamingConvention namingConvention) {
		assertAttributes();
		return super.getSortableAttributeCompoundByName(name, namingConvention);
	}

	@Nonnull
	@Override
	public Collection getSortableAttributeCompoundsForAttribute(@Nonnull String attributeName) {
		assertAttributes();
		return super.getSortableAttributeCompoundsForAttribute(attributeName);
	}

	@Override
	public void validate(@Nonnull CatalogSchemaContract catalogSchema, @Nonnull EntitySchema entitySchema) throws SchemaAlteringException {
		final String referencedEntityType = this.getReferencedEntityType();
		final Optional referencedEntityTypeSchema = catalogSchema.getEntitySchema(referencedEntityType);
		Stream referenceErrors = Stream.empty();
		if (referencedEntityTypeSchema.isEmpty()) {
			referenceErrors = Stream.concat(
				referenceErrors,
				Stream.of("Referenced entity type `" + referencedEntityType + "` is not present in catalog `" + catalogSchema.getName() + "` schema!"));
		}
		final Optional originalReference = referencedEntityTypeSchema.flatMap(it -> it.getReference(this.reflectedReferenceName));
		if (originalReference.isEmpty()) {
			referenceErrors = Stream.concat(
				referenceErrors,
				Stream.of(
					"Referenced entity type `" + referencedEntityType + "` " +
						"doesn't contain reference `" + this.reflectedReferenceName + "`, " +
						"which is reflected in reference `" + this.getName() + "`!")
			);
		} else if (!originalReference.get().getReferencedEntityType().equals(entitySchema.getName())) {
			referenceErrors = Stream.concat(
				referenceErrors,
				Stream.of(
					"Referenced entity type `" + referencedEntityType + "` " +
						"contains reference `" + this.reflectedReferenceName + "`, " +
						"but it targets different entity type `" + originalReference.get().getReferencedEntityType() + "` (expected `" + entitySchema.getName() + "`)!")
			);
		} else if (!originalReference.get().isReferencedEntityTypeManaged()) {
			referenceErrors = Stream.concat(
				referenceErrors,
				Stream.of(
					"Referenced entity type `" + referencedEntityType + "` " +
						"contains reference `" + this.reflectedReferenceName + "`, " +
						"but it's not managed entity type!")
			);
		} else if (this.reflectedReference == null) {
			referenceErrors = Stream.concat(
				referenceErrors,
				Stream.of(
					"Reflected reference schema `" + getName() + "` is not properly initialized!")
			);
		}

		if (
			this.isReflectedReferenceAvailable() ||
				this.attributesInheritanceBehavior == AttributeInheritanceBehavior.INHERIT_ONLY_SPECIFIED &&
					ArrayUtils.isEmpty(this.attributeInheritanceFilter)
		) {
			referenceErrors = Stream.concat(
				referenceErrors,
				validateAttributes(this.getAttributes())
			);
		}

		final List errors = referenceErrors.map(it -> "\t" + it).toList();
		if (!errors.isEmpty()) {
			throw new InvalidSchemaMutationException(
				"Reference schema `" + this.name + "` contains validation errors:\n" + String.join("\n", errors)
			);
		}
	}

	@Nonnull
	@Override
	public ReferenceSchemaContract withUpdatedReferencedEntityType(@Nonnull String newReferencedEntityType) {
		return new ReflectedReferenceSchema(
			this.getName(),
			this.getNameVariants(),
			this.descriptionInherited ? null : this.getDescription(),
			this.deprecatedInherited ? null : this.getDeprecationNotice(),
			this.cardinalityInherited ? null : this.getCardinality(),
			newReferencedEntityType,
			this.reflectedReferenceName,
			this.facetedInherited ? null : this.isFaceted(),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			attributesInheritanceBehavior,
			attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	@Nonnull
	@Override
	public ReferenceSchemaContract withUpdatedReferencedGroupType(@Nonnull String newReferencedGroupType) {
		throw new GenericEvitaInternalError(
			"Referenced group type cannot be modified on reflected reference schema!"
		);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		if (!super.equals(o)) return false;

		ReflectedReferenceSchema that = (ReflectedReferenceSchema) o;
		return descriptionInherited == that.descriptionInherited && deprecatedInherited == that.deprecatedInherited && cardinalityInherited == that.cardinalityInherited && facetedInherited == that.facetedInherited && attributesInheritanceBehavior == that.attributesInheritanceBehavior && reflectedReferenceName.equals(that.reflectedReferenceName) && Arrays.equals(attributeInheritanceFilter, that.attributeInheritanceFilter);
	}

	@Override
	public int hashCode() {
		int result = super.hashCode();
		result = 31 * result + reflectedReferenceName.hashCode();
		result = 31 * result + Boolean.hashCode(descriptionInherited);
		result = 31 * result + Boolean.hashCode(deprecatedInherited);
		result = 31 * result + Boolean.hashCode(cardinalityInherited);
		result = 31 * result + Boolean.hashCode(facetedInherited);
		result = 31 * result + attributesInheritanceBehavior.hashCode();
		result = 31 * result + Arrays.hashCode(attributeInheritanceFilter);
		return result;
	}

	@Nonnull
	public Optional getDeclaredAttribute(String attributeName) {
		// when attribute is inherited - return empty result
		if (this.reflectedReference != null && this.reflectedReference.getAttribute(attributeName).isPresent()) {
			return Optional.empty();
		}
		return super.getAttribute(attributeName);
	}

	@Nonnull
	public Map getDeclaredAttributes() {
		// when attribute is inherited - return empty result
		if (this.reflectedReference != null) {
			// this method always creates new map and is suboptimal, if it becomes bottleneck, we should memoize the product
			return intersect(
				super.getAttributes(),
				attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
			);
		} else {
			return super.getAttributes();
		}
	}

	@Nonnull
	public Map getDeclaredSortableAttributeCompounds() {
		// when attribute is inherited - return empty result
		if (this.reflectedReference != null) {
			// this method always creates new map and is suboptimal, if it becomes bottleneck, we should memoize the product
			return intersect(
				super.getSortableAttributeCompounds(),
				attributeName -> this.reflectedReference.getSortableAttributeCompound(attributeName).isPresent()
			);
		} else {
			return super.getSortableAttributeCompounds();
		}
	}

	@Nonnull
	public Optional getDeclaredSortableAttributeCompound(@Nonnull String name) {
		// when sortable attribute compound is inherited - return empty result
		if (this.reflectedReference != null && this.reflectedReference.getSortableAttributeCompound(name).isPresent()) {
			return Optional.empty();
		}
		return super.getSortableAttributeCompound(name);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param name new value of name property
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withName(@Nullable String name) {
		ClassifierUtils.validateClassifierFormat(ClassifierType.ENTITY, name);
		return new ReflectedReferenceSchema(
			name,
			NamingConvention.generate(name),
			this.description,
			this.deprecationNotice,
			this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			this.descriptionInherited,
			this.deprecatedInherited,
			this.cardinalityInherited,
			this.facetedInherited,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param description new value of description property
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withDescription(@Nullable String description) {
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			description,
			this.deprecationNotice,
			this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			description == null,
			this.deprecatedInherited,
			this.cardinalityInherited,
			this.facetedInherited,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param deprecationNotice new value of deprecation notice property
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withDeprecationNotice(@Nullable String deprecationNotice) {
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			this.description,
			deprecationNotice,
			this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			this.descriptionInherited,
			deprecationNotice == null,
			this.cardinalityInherited,
			this.facetedInherited,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param cardinality new value of cardinality property
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withCardinality(@Nullable Cardinality cardinality) {
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			this.description,
			this.deprecationNotice,
			cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			this.descriptionInherited,
			this.deprecatedInherited,
			cardinality == null,
			this.facetedInherited,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param faceted new value of faceted property
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withFaceted(@Nullable Boolean faceted) {
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			this.description,
			this.deprecationNotice,
			this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			this.descriptionInherited,
			this.deprecatedInherited,
			this.cardinalityInherited,
			faceted == null,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param inheritanceBehavior        attribute inheritance property
	 * @param attributeInheritanceFilter set of attributes inherited / excluded from inheritance
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withAttributeInheritance(
		@Nonnull AttributeInheritanceBehavior inheritanceBehavior,
		@Nonnull String... attributeInheritanceFilter
	) {
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			this.description,
			this.deprecationNotice,
			this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			this.descriptionInherited,
			this.deprecatedInherited,
			this.cardinalityInherited,
			this.facetedInherited,
			inheritanceBehavior,
			attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param newlyDeclaredAttributes new set of attributes declared on reflected reference itself
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withDeclaredAttributes(
		@Nonnull Map newlyDeclaredAttributes
	) {
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			this.description,
			this.deprecationNotice,
			this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				newlyDeclaredAttributes :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				union(
					newlyDeclaredAttributes,
					invertNecessaryAttributeTypes(this.reflectedReference.getAttributes()),
					this.attributesInheritanceBehavior,
					this.attributeInheritanceFilter
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getSortableAttributeCompounds() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getSortableAttributeCompounds(),
					sortableAttributeCompoundName -> this.reflectedReference.getSortableAttributeCompound(sortableAttributeCompoundName).isPresent()
				),
			this.descriptionInherited,
			this.deprecatedInherited,
			this.cardinalityInherited,
			this.facetedInherited,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param newlyDeclaredSortableAttributeCompounds new set of sortable attribute compounds declared on reflected reference itself
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchemaContract withDeclaredSortableAttributeCompounds(
		@Nonnull Map newlyDeclaredSortableAttributeCompounds
	) {
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			this.description,
			this.deprecationNotice,
			this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				super.getAttributes() :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				intersect(
					this.getAttributes(),
					attributeName -> this.reflectedReference.getAttribute(attributeName).isPresent()
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				newlyDeclaredSortableAttributeCompounds :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				union(
					newlyDeclaredSortableAttributeCompounds,
					this.reflectedReference.getSortableAttributeCompounds(),
					this.attributesInheritanceBehavior,
					this.attributeInheritanceFilter
				),
			this.descriptionInherited,
			this.deprecatedInherited,
			this.cardinalityInherited,
			this.facetedInherited,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			this.reflectedReference
		);
	}

	/**
	 * Creates a copy of this instance with different settings for:
	 *
	 * @param originalReference the schema this schema reflects
	 * @return copy of the schema with applied changes
	 */
	@Nonnull
	public ReflectedReferenceSchema withReferencedSchema(@Nonnull ReferenceSchemaContract originalReference) {
		Assert.isTrue(
			originalReference.isIndexed(),
			() -> new InvalidSchemaMutationException(
				"Referenced reference `" + originalReference.getName() +
					"` must be indexed in order to propagate changes to reflected reference `" + getName() + "`!"
			)
		);
		return new ReflectedReferenceSchema(
			this.name,
			this.nameVariants,
			this.descriptionInherited ? originalReference.getDescription() : this.description,
			this.deprecatedInherited ? originalReference.getDeprecationNotice() : this.deprecationNotice,
			this.cardinalityInherited ? originalReference.getCardinality() : this.cardinality,
			this.referencedEntityType,
			this.entityTypeNameVariants,
			this.referencedGroupType,
			this.groupTypeNameVariants,
			this.referencedGroupTypeManaged,
			this.reflectedReferenceName,
			this.facetedInherited ? originalReference.isFaceted() : this.faceted,
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				union(
					super.getAttributes(),
					invertNecessaryAttributeTypes(originalReference.getAttributes()),
					this.attributesInheritanceBehavior,
					this.attributeInheritanceFilter
				) :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				union(
					this.getDeclaredAttributes(),
					invertNecessaryAttributeTypes(originalReference.getAttributes()),
					this.attributesInheritanceBehavior,
					this.attributeInheritanceFilter
				),
			this.reflectedReference == null ?
				// when reflected reference is not present, only attributes unique for reflected ones are present
				union(
					super.getSortableAttributeCompounds(),
					originalReference.getSortableAttributeCompounds(),
					this.attributesInheritanceBehavior,
					this.attributeInheritanceFilter
				) :
				// if the reflected reference is present, attributes are merged using union function and we need to
				// filter them out to leave attributes added on the reflected reference only
				union(
					this.getDeclaredSortableAttributeCompounds(),
					originalReference.getSortableAttributeCompounds(),
					this.attributesInheritanceBehavior,
					this.attributeInheritanceFilter
				),
			this.descriptionInherited,
			this.deprecatedInherited,
			this.cardinalityInherited,
			this.facetedInherited,
			this.attributesInheritanceBehavior,
			this.attributeInheritanceFilter,
			originalReference
		);
	}

	/**
	 * Retrieves the set of inherited attributes from the original reference. When the reflected reference is not available,
	 * the set is empty.
	 *
	 * @return a set containing the names of inherited attributes
	 */
	@Nonnull
	public Set getInheritedAttributes() {
		return this.inheritedAttributes;
	}

	/**
	 * Verifies that the attributes of the reflected reference are available.
	 */
	private void assertAttributes() {
		Assert.isTrue(
			(this.attributesInheritanceBehavior == AttributeInheritanceBehavior.INHERIT_ONLY_SPECIFIED && ArrayUtils.isEmpty(this.attributeInheritanceFilter))
				|| this.reflectedReference != null,
			"Attributes of the reflected reference are inherited from the target reference, but the reflected reference is not available!"
		);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy