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

io.evitadb.api.requestResponse.data.structure.Entity 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.data.structure;

import io.evitadb.api.exception.EntityIsNotHierarchicalException;
import io.evitadb.api.exception.ReferenceNotFoundException;
import io.evitadb.api.query.Query;
import io.evitadb.api.query.filter.AttributeContains;
import io.evitadb.api.query.filter.AttributeEquals;
import io.evitadb.api.query.filter.EntityLocaleEquals;
import io.evitadb.api.query.filter.EntityPrimaryKeyInSet;
import io.evitadb.api.query.filter.FacetHaving;
import io.evitadb.api.query.filter.HierarchyWithin;
import io.evitadb.api.query.filter.PriceInPriceLists;
import io.evitadb.api.query.order.AttributeNatural;
import io.evitadb.api.query.order.PriceNatural;
import io.evitadb.api.query.require.AssociatedDataContent;
import io.evitadb.api.query.require.AttributeContent;
import io.evitadb.api.query.require.HierarchyContent;
import io.evitadb.api.query.require.HierarchyOfReference;
import io.evitadb.api.query.require.HierarchyOfSelf;
import io.evitadb.api.query.require.PriceContent;
import io.evitadb.api.query.require.PriceHistogram;
import io.evitadb.api.query.require.QueryPriceMode;
import io.evitadb.api.requestResponse.data.AssociatedDataContract;
import io.evitadb.api.requestResponse.data.AssociatedDataEditor.AssociatedDataBuilder;
import io.evitadb.api.requestResponse.data.AttributesEditor.AttributesBuilder;
import io.evitadb.api.requestResponse.data.EntityClassifierWithParent;
import io.evitadb.api.requestResponse.data.EntityEditor.EntityBuilder;
import io.evitadb.api.requestResponse.data.PriceContract;
import io.evitadb.api.requestResponse.data.PriceInnerRecordHandling;
import io.evitadb.api.requestResponse.data.PricesContract;
import io.evitadb.api.requestResponse.data.ReferenceContract;
import io.evitadb.api.requestResponse.data.SealedEntity;
import io.evitadb.api.requestResponse.data.Versioned;
import io.evitadb.api.requestResponse.data.mutation.LocalMutation;
import io.evitadb.api.requestResponse.data.mutation.associatedData.AssociatedDataMutation;
import io.evitadb.api.requestResponse.data.mutation.attribute.AttributeMutation;
import io.evitadb.api.requestResponse.data.mutation.parent.ParentMutation;
import io.evitadb.api.requestResponse.data.mutation.price.PriceMutation;
import io.evitadb.api.requestResponse.data.mutation.price.SetPriceInnerRecordHandlingMutation;
import io.evitadb.api.requestResponse.data.mutation.reference.ReferenceKey;
import io.evitadb.api.requestResponse.data.mutation.reference.ReferenceMutation;
import io.evitadb.api.requestResponse.data.structure.Price.PriceKey;
import io.evitadb.api.requestResponse.data.structure.predicate.AssociatedDataValueSerializablePredicate;
import io.evitadb.api.requestResponse.data.structure.predicate.AttributeValueSerializablePredicate;
import io.evitadb.api.requestResponse.data.structure.predicate.HierarchySerializablePredicate;
import io.evitadb.api.requestResponse.data.structure.predicate.LocaleSerializablePredicate;
import io.evitadb.api.requestResponse.data.structure.predicate.PriceContractSerializablePredicate;
import io.evitadb.api.requestResponse.data.structure.predicate.ReferenceContractSerializablePredicate;
import io.evitadb.api.requestResponse.extraResult.FacetSummary.FacetStatistics;
import io.evitadb.api.requestResponse.schema.AttributeSchemaContract;
import io.evitadb.api.requestResponse.schema.EntityAttributeSchemaContract;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.api.requestResponse.schema.NamedSchemaContract;
import io.evitadb.api.requestResponse.schema.ReferenceSchemaContract;
import io.evitadb.api.requestResponse.schema.dto.EntitySchema;
import io.evitadb.exception.EvitaInvalidUsageException;
import io.evitadb.utils.Assert;
import io.evitadb.utils.CollectionUtils;
import lombok.Getter;
import lombok.experimental.Delegate;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
import java.io.Serial;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;

/**
 * Based on our experience we've designed following data model for handling entities in evitaDB. Model is rather complex
 * but was designed to limit amount of data fetched from database and minimize an amount of data that are indexed and subject
 * to search.
 *
 * Minimal entity definition consists of:
 *
 * - entity type and
 * - primary key (even this is optional and may be autogenerated by the database).
 *
 * Other entity data is purely optional and may not be used at all.
 *
 * Class is immutable on purpose - we want to support caching the entities in a shared cache and accessed by many threads.
 * For altering the contents use {@link InitialEntityBuilder}.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
 */
@Immutable
@ThreadSafe
public class Entity implements SealedEntity {
	@Serial private static final long serialVersionUID = 8637366499361070438L;

	/**
	 * Contains version of this object and gets increased with any (direct) entity update. Allows to execute
	 * optimistic locking i.e. avoiding parallel modifications.
	 */
	final int version;
	/**
	 * Serializable type of entity. Using Enum type is highly recommended for this key.
	 * Entity type is main sharding key - all data of entities with same type are stored in separated index. Within the
	 * entity type entity is uniquely represented by primary key.
	 * Type is specified in each lookup {@link Query#getCollection()}
	 */
	@Getter @Nonnull final String type;
	/**
	 * Contains definition of the entity.
	 */
	@Getter @Nonnull final EntitySchemaContract schema;
	/**
	 * Unique Integer positive number (max. 263-1) representing the entity. Can be used for fast lookup for
	 * entity (entities). Primary key must be unique within the same entity type.
	 * May be left empty if it should be auto generated by the database.
	 * Entities can by looked up by primary key by using query {@link EntityPrimaryKeyInSet}
	 */
	@Getter @Nullable final Integer primaryKey;
	/**
	 * Entities may be organized in hierarchical fashion. That means that entity may refer to single parent entity and may be
	 * referred by multiple child entities. Hierarchy is always composed of entities of same type.
	 * Each entity must be part of at most single hierarchy (tree).
	 * Hierarchy can limit returned entities by using filtering constraints {@link HierarchyWithin}. It's also used for
	 * computation of extra data - such as {@link HierarchyContent}. It can also invert type of returned entities in
	 * case requirement {@link HierarchyOfSelf} is used.
	 */
	@Nullable final Integer parent;
	/**
	 * Contains true if the entity is allowed to have parents by the schema.
	 */
	final boolean withHierarchy;
	/**
	 * The reference refers to other entities (of same or different entity type).
	 * Allows entity filtering (but not sorting) of the entities by using {@link FacetHaving} query
	 * and statistics computation if when {@link FacetStatistics} requirement is used. Reference
	 * is uniquely represented by int positive number (max. 263-1) and {@link Serializable} entity type and can be
	 * part of multiple reference groups, that are also represented by int and {@link Serializable} entity type.
	 *
	 * Reference id in one entity is unique and belongs to single reference group id. Among multiple entities reference may be part
	 * of different reference groups. Referenced entity type may represent type of another Evita entity or may refer
	 * to anything unknown to Evita that posses unique int key and is maintained by external systems (fe. tag assignment,
	 * group assignment, category assignment, stock assignment and so on). Not all these data needs to be present in
	 * Evita.
	 *
	 * References may carry additional key-value data linked to this entity relation (fe. item count present on certain stock).
	 */
	final Map references;
	/**
	 * Contains set of all {@link ReferenceSchemaContract#getName()} defined in entity {@link EntitySchemaContract}.
	 */
	final Set referencesDefined;
	/**
	 * Entity (global) attributes allows defining set of data that are fetched in bulk along with the entity body.
	 * Attributes may be indexed for fast filtering ({@link AttributeSchemaContract#isFilterable()}) or can be used to sort along
	 * ({@link AttributeSchemaContract#isSortable()}). Attributes are not automatically indexed in order not to waste precious
	 * memory space for data that will never be used in search queries.
	 *
	 * Filtering in attributes is executed by using constraints like {@link io.evitadb.api.query.filter.And},
	 * {@link io.evitadb.api.query.filter.Not}, {@link AttributeEquals}, {@link AttributeContains}
	 * and many others. Sorting can be achieved with {@link AttributeNatural} or others.
	 *
	 * Attributes are not recommended for bigger data as they are all loaded at once when {@link AttributeContent}
	 * requirement is used. Large data that are occasionally used store in {@link @AssociatedData}.
	 */
	@Delegate(types = EntityAttributes.class) final EntityAttributes attributes;
	/**
	 * Associated data carry additional data entries that are never used for filtering / sorting but may be needed to be fetched
	 * along with entity in order to present data to the target consumer (i.e. user / API / bot). Associated data may be stored
	 * in slower storage and may contain wide range of data types - from small ones (i.e. numbers, strings, dates) up to large
	 * binary arrays representing entire files (i.e. pictures, documents).
	 *
	 * The search query must contain specific {@link AssociatedDataContent} requirement in order
	 * associated data are fetched along with the entity. Associated data are stored and fetched separately by their name.
	 */
	@Delegate(types = AssociatedDataContract.class) final AssociatedData associatedData;
	/**
	 * Prices are specific to a very few entities, but because correct price computation is very complex in e-commerce
	 * systems and highly affects performance of the entities filtering and sorting, they deserve first class support
	 * in entity model. It is pretty common in B2B systems single product has assigned dozens of prices for the different
	 * customers.
	 * 

* Specifying prices on entity allows usage of {@link io.evitadb.api.query.filter.PriceValidIn}, * {@link io.evitadb.api.query.filter.PriceBetween}, {@link QueryPriceMode} * and {@link PriceInPriceLists} filtering constraints and also {@link PriceNatural}, * ordering of the entities. Additional requirements * {@link PriceHistogram}, {@link PriceContent} * can be used in query as well. */ @Delegate(types = PricesContract.class, excludes = Versioned.class) @Nonnull final Prices prices; /** * Contains set of all {@link Locale} that were used for localized {@link Attributes} or {@link AssociatedData} of * this particular entity. * * Enables using {@link EntityLocaleEquals} filtering query in query. */ @Getter final Set locales; /** * Contains TRUE if entity was dropped - i.e. removed. Entities is not removed (unless tidying process * does it), but are lying among other entities with tombstone flag. Dropped entities can be overwritten by * a revived entity continuing with the versioning where it was stopped for the last time. */ private final boolean dropped; /** * Contains map of all references by their name. This map is used for fast lookup of the references by their name * and is initialized lazily on first request. */ private Map> referencesByName; /** * This method is for internal purposes only. It could be used for reconstruction of original Entity 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 Entity _internalBuild( @Nullable Integer primaryKey, @Nullable Integer version, @Nonnull EntitySchemaContract entitySchema, @Nullable Integer parent, @Nonnull Collection references, @Nonnull EntityAttributes attributes, @Nonnull AssociatedData associatedData, @Nonnull Prices prices, @Nonnull Set locales ) { return new Entity( ofNullable(version).orElse(1), entitySchema, primaryKey, parent, references, attributes, associatedData, prices, locales, false ); } /** * This method is for internal purposes only. It could be used for reconstruction of original Entity 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 Entity _internalBuild( @Nullable Integer primaryKey, @Nullable Integer version, @Nonnull EntitySchemaContract entitySchema, @Nullable Integer parent, @Nonnull Collection references, @Nonnull EntityAttributes attributes, @Nonnull AssociatedData associatedData, @Nonnull Prices prices, @Nonnull Set locales, @Nonnull Set referencesDefined, boolean withHierarchy, boolean dropped ) { return new Entity( ofNullable(version).orElse(1), entitySchema, primaryKey, parent, references, attributes, associatedData, prices, locales, referencesDefined, withHierarchy, dropped ); } /** * This method is for internal purposes only. It could be used for reconstruction of original Entity 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 Entity _internalBuild( int version, int primaryKey, @Nonnull EntitySchemaContract schema, @Nullable Integer parent, @Nonnull Collection references, @Nonnull EntityAttributes attributes, @Nonnull AssociatedData associatedData, @Nonnull Prices prices, @Nonnull Set locales, boolean dropped ) { return new Entity( version, schema, primaryKey, parent, references, attributes, associatedData, prices, locales, dropped ); } /** * This method is for internal purposes only. It could be used for reconstruction of original Entity 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 Entity _internalBuild( @Nonnull Entity entity, int version, int primaryKey, @Nonnull EntitySchemaContract schema, @Nullable Integer parent, @Nullable Collection references, @Nullable EntityAttributes attributes, @Nullable AssociatedData associatedData, @Nullable Prices prices, @Nullable Set locales, boolean dropped ) { return new Entity( version, schema, primaryKey, ofNullable(parent).orElse(entity.parent), ofNullable(references).orElse(entity.references.values()), ofNullable(attributes).orElse(entity.attributes), ofNullable(associatedData).orElse(entity.associatedData), ofNullable(prices).orElse(entity.prices), ofNullable(locales).orElse(entity.locales), dropped ); } /** * Method allows mutation of the existing entity by the set of local mutations. If the mutations don't change any * data (it may happen that the requested change was already applied by someone else) the very same entity is * returned in the response. */ @Nonnull public static Entity mutateEntity( @Nonnull EntitySchemaContract entitySchema, @Nullable Entity entity, @Nonnull Collection> localMutations ) { final Optional possibleEntity = ofNullable(entity); final Integer oldParent = ofNullable(entity).map(it -> it.parent).orElse(null); Integer newParent = oldParent; PriceInnerRecordHandling newPriceInnerRecordHandling = null; final Map newAttributes = CollectionUtils.createHashMap(localMutations.size()); final Map newAssociatedData = CollectionUtils.createHashMap(localMutations.size()); final Map newReferences = CollectionUtils.createHashMap(localMutations.size()); final Map newPrices = CollectionUtils.createHashMap(localMutations.size()); for (LocalMutation localMutation : localMutations) { if (localMutation instanceof ParentMutation parentMutation) { newParent = mutateHierarchyPlacement(entitySchema, possibleEntity, parentMutation); } else if (localMutation instanceof AttributeMutation attributeMutation) { mutateAttributes(entitySchema, possibleEntity, newAttributes, attributeMutation); } else if (localMutation instanceof AssociatedDataMutation associatedDataMutation) { mutateAssociatedData(entitySchema, possibleEntity, newAssociatedData, associatedDataMutation); } else if (localMutation instanceof ReferenceMutation referenceMutation) { mutateReferences(entitySchema, possibleEntity, newReferences, referenceMutation); } else if (localMutation instanceof PriceMutation priceMutation) { mutatePrices(entitySchema, possibleEntity, newPrices, priceMutation); } else if (localMutation instanceof SetPriceInnerRecordHandlingMutation innerRecordHandlingMutation) { newPriceInnerRecordHandling = mutateInnerPriceRecordHandling(entitySchema, possibleEntity, innerRecordHandlingMutation); } } // create or reuse existing attribute container final EntityAttributes newAttributeContainer = recreateAttributeContainer(entitySchema, possibleEntity, newAttributes); // create or reuse existing associated data container final AssociatedData newAssociatedDataContainer = recreateAssociatedDataContainer(entitySchema, possibleEntity, newAssociatedData); // create or reuse existing reference container final ReferenceTuple mergedReferences = recreateReferences(possibleEntity, newReferences); // create or reuse existing prices final Prices priceContainer = recreatePrices(entitySchema, possibleEntity, newPriceInnerRecordHandling, newPrices); // aggregate entity locales final Set entityLocales = new HashSet<>(newAttributeContainer.getAttributeLocales()); entityLocales.addAll(newAssociatedDataContainer.getAssociatedDataLocales()); if (!Objects.equals(newParent, oldParent) || newPriceInnerRecordHandling != null || !newAttributes.isEmpty() || !newAssociatedData.isEmpty() || !newPrices.isEmpty() || !newReferences.isEmpty() ) { return new Entity( possibleEntity.map(it -> it.version() + 1).orElse(1), entitySchema, possibleEntity.map(Entity::getPrimaryKey).orElse(null), newParent, mergedReferences.references(), newAttributeContainer, newAssociatedDataContainer, priceContainer, entityLocales, mergedReferences.referencesDefined(), entitySchema.isWithHierarchy() || newParent != null, false ); } else if (entity == null) { return new Entity(entitySchema.getName(), null); } else { return entity; } } /** * Method allows to create copy of the entity object with up-to-date schema definition. Data of the original * entity are kept untouched. */ @Nonnull public static EntityDecorator decorate( @Nonnull Entity entity, @Nonnull EntitySchemaContract entitySchema, @Nullable EntityClassifierWithParent parentEntity, @Nonnull LocaleSerializablePredicate localePredicate, @Nonnull HierarchySerializablePredicate hierarchyPredicate, @Nonnull AttributeValueSerializablePredicate attributePredicate, @Nonnull AssociatedDataValueSerializablePredicate associatedDataValuePredicate, @Nonnull ReferenceContractSerializablePredicate referencePredicate, @Nonnull PriceContractSerializablePredicate pricePredicate, @Nonnull OffsetDateTime alignedNow, @Nullable ReferenceFetcher referenceFetcher ) { return referenceFetcher == null || referenceFetcher == ReferenceFetcher.NO_IMPLEMENTATION ? new EntityDecorator( entity, entitySchema, parentEntity, localePredicate, hierarchyPredicate, attributePredicate, associatedDataValuePredicate, referencePredicate, pricePredicate, alignedNow ) : new EntityDecorator( entity, entitySchema, parentEntity, localePredicate, hierarchyPredicate, attributePredicate, associatedDataValuePredicate, referencePredicate, pricePredicate, alignedNow, referenceFetcher ); } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nonnull private static Prices recreatePrices( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nullable PriceInnerRecordHandling newPriceInnerRecordHandling, @Nonnull Map newPrices ) { final Prices priceContainer; if (newPrices.isEmpty()) { priceContainer = ofNullable(newPriceInnerRecordHandling) .map(npirc -> possibleEntity .map(it -> new Prices(entitySchema, it.version() + 1, it.getPrices(), npirc, !it.getPrices().isEmpty())) .orElseGet(() -> new Prices(entitySchema, 1, Collections.emptyList(), npirc, false)) ).orElseGet(() -> possibleEntity .map(it -> it.prices) .orElseGet(() -> new Prices(entitySchema, 1, Collections.emptyList(), PriceInnerRecordHandling.NONE, false))); } else { final List mergedPrices = Stream.concat( possibleEntity.map(Entity::getPrices).orElseGet(Collections::emptyList) .stream() .filter(it -> !newPrices.containsKey(it.priceKey())), newPrices.values().stream() ).toList(); priceContainer = ofNullable(newPriceInnerRecordHandling) .map(npirc -> possibleEntity .map(it -> new Prices(entitySchema, it.version() + 1, mergedPrices, npirc, !mergedPrices.isEmpty())) .orElseGet(() -> new Prices(entitySchema, 1, mergedPrices, npirc, !mergedPrices.isEmpty())) ).orElseGet(() -> possibleEntity .map(it -> new Prices(entitySchema, it.version + 1, mergedPrices, it.getPriceInnerRecordHandling(), !mergedPrices.isEmpty())) .orElseGet(() -> new Prices(entitySchema, 1, mergedPrices, PriceInnerRecordHandling.NONE, !mergedPrices.isEmpty()))); } return priceContainer; } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nonnull private static ReferenceTuple recreateReferences( @Nonnull Optional possibleEntity, @Nonnull Map newReferences ) { final Set mergedTypes; final Collection mergedReferences; if (newReferences.isEmpty()) { mergedTypes = possibleEntity .map(Entity::getSchema) .map(EntitySchemaContract::getReferences) .map(Map::keySet) .orElseGet(Collections::emptySet); mergedReferences = possibleEntity .map(Entity::getReferences) .orElseGet(Collections::emptyList); } else { mergedTypes = Stream.concat( possibleEntity .map(Entity::getSchema) .map(EntitySchemaContract::getReferences) .map(Map::keySet) .orElseGet(Collections::emptySet) .stream(), newReferences.values() .stream() .map(ReferenceContract::getReferenceName) ) .collect(Collectors.toSet()); mergedReferences = Stream.concat( possibleEntity.map(Entity::getReferences).orElseGet(Collections::emptyList) .stream() .filter(it -> !newReferences.containsKey(it.getReferenceKey())), newReferences.values().stream() ).toList(); } return new ReferenceTuple( mergedReferences, mergedTypes ); } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nonnull private static AssociatedData recreateAssociatedDataContainer( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull Map newAssociatedData ) { final AssociatedData newAssociatedDataContainer; if (newAssociatedData.isEmpty()) { newAssociatedDataContainer = possibleEntity .map(it -> it.associatedData) .orElseGet(() -> new AssociatedData(entitySchema)); } else { newAssociatedDataContainer = new AssociatedData( entitySchema, Stream.concat( possibleEntity.map(Entity::getAssociatedDataValues).orElseGet(Collections::emptyList) .stream() .filter(it -> !newAssociatedData.containsKey(it.key())), newAssociatedData.values().stream() ).toList(), Stream.concat( entitySchema.getAssociatedData().values().stream(), newAssociatedData.values().stream() .filter(it -> !entitySchema.getAssociatedData().containsKey(it.key().associatedDataName())) .map(AssociatedDataBuilder::createImplicitSchema) ) .collect( Collectors.toMap( NamedSchemaContract::getName, Function.identity() ) ) ); } return newAssociatedDataContainer; } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nonnull private static EntityAttributes recreateAttributeContainer( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull Map newAttributes ) { final EntityAttributes newAttributeContainer; if (newAttributes.isEmpty()) { newAttributeContainer = possibleEntity .map(it -> it.attributes) .orElseGet(() -> new EntityAttributes(entitySchema)); } else { final Map attributes = Stream.concat( possibleEntity.map(Entity::getAttributeValues).orElseGet(Collections::emptyList) .stream() .filter(it -> !newAttributes.containsKey(it.key())), newAttributes.values().stream() ) .collect( Collectors.toMap( AttributeValue::key, Function.identity(), (o, n) -> { throw new EvitaInvalidUsageException("Duplicate attribute key " + o.key()); }, LinkedHashMap::new ) ); final Map attributeTypes = Stream.concat( entitySchema.getAttributes().values().stream(), newAttributes.values().stream() .filter(it -> !entitySchema.getAttributes().containsKey(it.key().attributeName())) .map(AttributesBuilder::createImplicitEntityAttributeSchema) ) .collect( Collectors.toMap( NamedSchemaContract::getName, Function.identity(), (o, n) -> { throw new EvitaInvalidUsageException("Duplicate attribute key " + o.getName()); }, LinkedHashMap::new ) ); newAttributeContainer = new EntityAttributes( entitySchema, attributes, attributeTypes ); } return newAttributeContainer; } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nullable private static PriceInnerRecordHandling mutateInnerPriceRecordHandling( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull SetPriceInnerRecordHandlingMutation innerRecordHandlingMutation ) { PriceInnerRecordHandling newPriceInnerRecordHandling; final PricesContract existingPrices = possibleEntity.map(it -> it.prices).orElse(null); final PricesContract newPriceContainer = returnIfChanged( existingPrices, innerRecordHandlingMutation.mutateLocal(entitySchema, existingPrices) ); newPriceInnerRecordHandling = ofNullable(newPriceContainer) .map(PricesContract::getPriceInnerRecordHandling) .orElse(null); return newPriceInnerRecordHandling; } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private static void mutatePrices( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull Map newPrices, @Nonnull PriceMutation priceMutation ) { final PriceContract existingPriceValue = possibleEntity .flatMap(it -> it.getPrice(priceMutation.getPriceKey())) .orElse(null); ofNullable( returnIfChanged( existingPriceValue, priceMutation.mutateLocal(entitySchema, existingPriceValue) ) ).ifPresent(it -> newPrices.put(it.priceKey(), it)); } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private static void mutateReferences( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull Map newReferences, @Nonnull ReferenceMutation referenceMutation) { final ReferenceContract existingReferenceValue = ofNullable(newReferences.get(referenceMutation.getReferenceKey())) .or(() -> possibleEntity.flatMap(it -> it.getReferenceWithoutSchemaCheck(referenceMutation.getReferenceKey()))) .orElse(null); ofNullable( returnIfChanged( existingReferenceValue, referenceMutation.mutateLocal(entitySchema, existingReferenceValue) ) ).ifPresent(it -> newReferences.put(it.getReferenceKey(), it)); } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private static void mutateAssociatedData( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull Map newAssociatedData, @Nonnull AssociatedDataMutation associatedDataMutation ) { final AssociatedDataValue existingAssociatedDataValue = possibleEntity .flatMap(it -> { final AssociatedDataKey associatedDataKey = associatedDataMutation.getAssociatedDataKey(); // we need to do this because new associated data can be added on the fly and getting them would trigger // an exception return it.getAssociatedDataNames().contains(associatedDataKey.associatedDataName()) ? it.getAssociatedDataValue(associatedDataKey) : empty(); }) .orElse(null); ofNullable( returnIfChanged( existingAssociatedDataValue, associatedDataMutation.mutateLocal(entitySchema, existingAssociatedDataValue) ) ).ifPresent(it -> newAssociatedData.put(it.key(), it)); } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private static void mutateAttributes( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull Map newAttributes, @Nonnull AttributeMutation attributeMutation ) { final AttributeValue existingAttributeValue = possibleEntity .flatMap(it -> { final AttributeKey attributeKey = attributeMutation.getAttributeKey(); // we need to do this because new attributes can be added on the fly and getting them would trigger // an exception return it.getAttributeNames().contains(attributeKey.attributeName()) ? it.getAttributeValue(attributeKey) : empty(); }) .orElse(null); ofNullable( returnIfChanged( existingAttributeValue, attributeMutation.mutateLocal(entitySchema, existingAttributeValue) ) ).ifPresent(it -> newAttributes.put(it.key(), it)); } /** * Helper method for {@link #mutateEntity(EntitySchemaContract, Entity, Collection)} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nullable private static Integer mutateHierarchyPlacement( @Nonnull EntitySchemaContract entitySchema, @Nonnull Optional possibleEntity, @Nonnull ParentMutation parentMutation ) { OptionalInt newParent; final OptionalInt existingPlacement = possibleEntity .map(Entity::getParent) .orElse(OptionalInt.empty()); newParent = parentMutation.mutateLocal(entitySchema, existingPlacement); return newParent.stream().boxed().findAny().orElse(null); } /** * Method will check whether the original value is exactly same as mutated value (the version id is compared). * If not the NULL is returned instead of `mutatedValue`. */ @Nullable private static T returnIfChanged(@Nullable T originalValue, @Nonnull T mutatedValue) { if (mutatedValue.version() > ofNullable(originalValue).map(Versioned::version).orElse(0)) { return mutatedValue; } else { return null; } } /** * Entities are not meant to be constructed by the client code. Use {@link InitialEntityBuilder} to create new or update * existing entities. */ private Entity( int version, @Nonnull EntitySchemaContract schema, @Nullable Integer primaryKey, @Nullable Integer parent, @Nonnull Collection references, @Nonnull EntityAttributes attributes, @Nonnull AssociatedData associatedData, @Nonnull Prices prices, @Nonnull Set locales, boolean dropped ) { this( version, schema, primaryKey, parent, references, attributes, associatedData, prices, locales, schema.getReferences().keySet(), schema.isWithHierarchy(), dropped ); } /** * Entities are not meant to be constructed by the client code. Use {@link InitialEntityBuilder} to create new or update * existing entities. */ private Entity( int version, @Nonnull EntitySchemaContract schema, @Nullable Integer primaryKey, @Nullable Integer parent, @Nonnull Collection references, @Nonnull EntityAttributes attributes, @Nonnull AssociatedData associatedData, @Nonnull Prices prices, @Nonnull Set locales, @Nonnull Set referencesDefined, boolean withHierarchy, boolean dropped ) { this.version = version; this.type = schema.getName(); this.schema = schema; this.primaryKey = primaryKey; this.parent = parent; this.withHierarchy = withHierarchy; this.references = Collections.unmodifiableMap( references .stream() .collect( Collectors.toMap( ReferenceContract::getReferenceKey, Function.identity(), (o, o2) -> { throw new EvitaInvalidUsageException("Sanity check: " + o + ", " + o2); }, LinkedHashMap::new ) ) ); this.referencesDefined = referencesDefined; this.attributes = attributes; this.associatedData = associatedData; this.prices = prices; this.locales = Collections.unmodifiableSet(locales); this.dropped = dropped; } public Entity(@Nonnull String type, @Nullable Integer primaryKey) { this.version = 1; this.type = type; this.schema = EntitySchema._internalBuild(type); this.primaryKey = primaryKey; this.parent = null; this.withHierarchy = this.schema.isWithHierarchy(); this.references = Collections.emptyMap(); this.referencesDefined = Collections.emptySet(); this.attributes = new EntityAttributes(this.schema); this.associatedData = new AssociatedData(this.schema); this.prices = new io.evitadb.api.requestResponse.data.structure.Prices( this.schema, 1, Collections.emptySet(), PriceInnerRecordHandling.NONE ); this.locales = Collections.emptySet(); this.dropped = false; } @Override public boolean parentAvailable() { return withHierarchy; } /** * Returns hierarchy information about the entity. Hierarchy information allows to compose hierarchy tree composed * of entities of the same type. Referenced entity is always entity of the same type. Referenced entity must be * already present in the evitaDB and must also have hierarchy placement set. Root `parentPrimaryKey` (i.e. parent * for top-level hierarchical placements) is null. * * Entities may be organized in hierarchical fashion. That means that entity may refer to single parent entity and * may be referred by multiple child entities. Hierarchy is always composed of entities of same type. * Each entity must be part of at most single hierarchy (tree). * * Hierarchy can limit returned entities by using filtering constraints {@link HierarchyWithin}. It's also used for * computation of extra data - such as {@link HierarchyOfSelf}. It can also invert type of returned entities in case * requirement {@link HierarchyOfReference} is used. * * @throws EntityIsNotHierarchicalException when {@link EntitySchemaContract#isWithHierarchy()} is false */ @Nonnull public OptionalInt getParent() throws EntityIsNotHierarchicalException { Assert.isTrue( withHierarchy, () -> new EntityIsNotHierarchicalException(schema.getName()) ); return parent == null ? OptionalInt.empty() : OptionalInt.of(parent); } @Nonnull @Override public Optional getParentEntity() { Assert.isTrue( withHierarchy, () -> new EntityIsNotHierarchicalException(schema.getName()) ); return ofNullable(parent) .map(it -> new EntityReferenceWithParent(type, it, null)); } @Override public boolean referencesAvailable() { return true; } @Override public boolean referencesAvailable(@Nonnull String referenceName) { return true; } @Nonnull @Override public Collection getReferences() { return references.values(); } @Nonnull @Override public Collection getReferences(@Nonnull String referenceName) { checkReferenceName(referenceName); if (this.referencesByName == null) { this.referencesByName = references .entrySet() .stream() .collect( Collectors.groupingBy( it -> it.getKey().referenceName(), Collectors.mapping( Entry::getValue, Collectors.toList() ) ) ); } return ofNullable(referencesByName.get(referenceName)) .orElse(Collections.emptyList()); } @Nonnull @Override public Optional getReference(@Nonnull String referenceName, int referencedEntityId) { checkReferenceName(referenceName); return ofNullable(references.get(new ReferenceKey(referenceName, referencedEntityId))); } @Nonnull @Override public Set getAllLocales() { return locales; } /** * Checks whether the reference is defined in the schema or is otherwise known. */ public void checkReferenceName(@Nonnull String referenceName) { Assert.isTrue( referencesDefined.contains(referenceName), () -> new ReferenceNotFoundException(referenceName, schema) ); } /** * Returns reference contract without checking the existence in the schema. * Part of the private API. */ @Nullable public Optional getReferenceWithoutSchemaCheck(@Nonnull ReferenceKey referenceKey) { return ofNullable(references.get(referenceKey)); } @Nullable public Optional getReference(@Nonnull ReferenceKey referenceKey) { checkReferenceName(referenceKey.referenceName()); return ofNullable(references.get(referenceKey)); } @Override public boolean dropped() { return dropped; } @Override public int version() { return version; } @Nonnull @Override public EntityBuilder openForWrite() { return new ExistingEntityBuilder(this); } @Nonnull @Override public EntityBuilder withMutations(@Nonnull LocalMutation... localMutations) { return new ExistingEntityBuilder( this, Arrays.asList(localMutations) ); } @Nonnull @Override public EntityBuilder withMutations(@Nonnull Collection> localMutations) { return new ExistingEntityBuilder( this, localMutations ); } @Override public int hashCode() { int result = 1; result = 31 * result + version; result = 31 * result + type.hashCode(); result = 31 * result + (primaryKey == null ? 0 : primaryKey.hashCode()); return result; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Entity entity = (Entity) o; return version == entity.version && type.equals(entity.type) && Objects.equals(primaryKey, entity.primaryKey); } @Override public String toString() { return describe(); } /** * DTO for passing merged references and their types. */ private record ReferenceTuple( @Nonnull Collection references, @Nonnull Set referencesDefined ) { } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy