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

io.evitadb.api.requestResponse.data.structure.Prices 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.ContextMissingException;
import io.evitadb.api.exception.EntityHasNoPricesException;
import io.evitadb.api.exception.UnexpectedResultCountException;
import io.evitadb.api.query.filter.PriceInPriceLists;
import io.evitadb.api.query.order.PriceNatural;
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.ContentComparator;
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.Versioned;
import io.evitadb.api.requestResponse.data.structure.Price.PriceKey;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.api.requestResponse.schema.EvolutionMode;
import io.evitadb.exception.GenericEvitaInternalError;
import io.evitadb.utils.Assert;
import lombok.EqualsAndHashCode;
import lombok.Getter;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serial;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.Currency;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import static java.util.Optional.ofNullable;

/**
 * Entity prices container allows defining set of prices of the entity.
 * Attributes may be indexed for fast filtering ({@link Price#indexed()}). Prices are not automatically indexed
 * in order not to waste precious memory space for data that will never be used in search queries.
 * 

* Filtering in prices is executed by using constraints like {@link io.evitadb.api.query.filter.PriceBetween}, * {@link io.evitadb.api.query.filter.PriceValidIn}, {@link PriceInPriceLists} or * {@link QueryPriceMode}. *

* 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 InitialPricesBuilder}. * * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021 */ @EqualsAndHashCode(of = "version") public class Prices implements PricesContract, Versioned, ContentComparator { @Serial private static final long serialVersionUID = -2717054691549391374L; /** * Definition of the entity schema. */ final EntitySchemaContract entitySchema; /** * Contains true if the entity is allowed to have prices by the schema. */ final boolean withPrice; /** * 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; /** * 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. */ final Map priceIndex; /** * Price inner record handling controls how prices that share same `inner entity id` will behave during filtering and sorting. */ @Getter final PriceInnerRecordHandling priceInnerRecordHandling; public Prices( @Nonnull EntitySchemaContract entitySchema, @Nonnull PriceInnerRecordHandling priceInnerRecordHandling ) { this.entitySchema = entitySchema; this.withPrice = entitySchema.isWithPrice(); this.version = 1; this.priceIndex = Collections.emptyMap(); this.priceInnerRecordHandling = priceInnerRecordHandling; } public Prices( @Nonnull EntitySchemaContract entitySchema, @Nonnull Collection prices, @Nonnull PriceInnerRecordHandling priceInnerRecordHandling ) { this.entitySchema = entitySchema; this.withPrice = entitySchema.isWithPrice(); this.version = 1; this.priceIndex = Collections.unmodifiableMap( prices .stream() .collect( Collectors.toMap( PriceContract::priceKey, Function.identity(), (oldValue, newValue) -> { throw new GenericEvitaInternalError("Duplicate price key " + oldValue.priceKey()); }, () -> new LinkedHashMap<>(prices.size()) ) ) ); this.priceInnerRecordHandling = priceInnerRecordHandling; } public Prices( @Nonnull EntitySchemaContract entitySchema, int version, @Nonnull Collection prices, @Nonnull PriceInnerRecordHandling priceInnerRecordHandling ) { this(entitySchema, version, prices, priceInnerRecordHandling, entitySchema.isWithPrice()); } public Prices( @Nonnull EntitySchemaContract entitySchema, int version, @Nonnull Collection prices, @Nonnull PriceInnerRecordHandling priceInnerRecordHandling, boolean withPrice ) { this.entitySchema = entitySchema; this.withPrice = withPrice; this.version = version; this.priceIndex = Collections.unmodifiableMap( prices .stream() .collect( Collectors.toMap( PriceContract::priceKey, Function.identity(), (oldValue, newValue) -> { throw new GenericEvitaInternalError("Duplicate price key " + oldValue.priceKey()); }, () -> new LinkedHashMap<>(prices.size()) ) ) ); this.priceInnerRecordHandling = priceInnerRecordHandling; } @Override public int version() { return version; } /** * Returns price by its business key identification. */ @Nonnull @Override public Optional getPrice(@Nonnull PriceKey priceKey) { Assert.isTrue( withPrice, () -> new EntityHasNoPricesException(entitySchema.getName()) ); return Optional.ofNullable(priceIndex.get(priceKey)); } /** * Returns price by its business key identification. */ @Nonnull public Optional getPrice(int priceId, @Nonnull String priceList, @Nonnull Currency currency) { Assert.isTrue( withPrice, () -> new EntityHasNoPricesException(entitySchema.getName()) ); return Optional.ofNullable(priceIndex.get(new PriceKey(priceId, priceList, currency))); } @Nonnull @Override public Optional getPrice(@Nonnull String priceList, @Nonnull Currency currency) throws UnexpectedResultCountException, ContextMissingException { Assert.isTrue( withPrice, () -> new EntityHasNoPricesException(entitySchema.getName()) ); final List matchingPrices = priceIndex.entrySet() .stream() .filter(it -> it.getKey().priceList().equals(priceList) && it.getKey().currency().equals(currency)) .map(Entry::getValue) .toList(); if (matchingPrices.size() > 1) { throw new UnexpectedResultCountException( matchingPrices.size(), "More than one price found for price list `" + priceList + "` and currency `" + currency + "`." ); } return matchingPrices.isEmpty() ? Optional.empty() : Optional.of(matchingPrices.get(0)); } @Override public boolean pricesAvailable() { return withPrice; } @Override public boolean isPriceForSaleContextAvailable() { return false; } @Nonnull @Override public Optional getPriceForSaleContext() { return Optional.empty(); } @Nonnull @Override public Optional getPriceForSale() throws ContextMissingException { throw new ContextMissingException(); } @Nonnull @Override public Optional getPriceForSaleIfAvailable() { return Optional.empty(); } @Nonnull @Override public List getAllPricesForSale() { throw new ContextMissingException(); } @Override public boolean hasPriceInInterval(@Nonnull BigDecimal from, @Nonnull BigDecimal to, @Nonnull QueryPriceMode queryPriceMode) throws ContextMissingException { throw new ContextMissingException(); } /** * Returns all prices of the entity. */ @Nonnull public Collection getPrices() { Assert.isTrue( this.withPrice || this.entitySchema.allows(EvolutionMode.ADDING_PRICES), () -> new EntityHasNoPricesException(entitySchema.getName()) ); return priceIndex.values(); } /** * Returns all prices indexed by key. */ @Nonnull public Map getPriceIndex() { return this.priceIndex; } /** * Returns true when there is no single price defined. */ public boolean isEmpty() { return this.priceIndex.isEmpty(); } /** * Returns price by business key without checking if the price is allowed by the schema. * Method is part of PRIVATE API. */ @Nonnull public Optional getPriceWithoutSchemaCheck(@Nonnull PriceKey priceKey) { return ofNullable(priceIndex.get(priceKey)); } /** * Method returns true if any prices inner data differs from other prices object. */ @Override public boolean differsFrom(@Nullable Prices otherPrices) { if (this == otherPrices) return false; if (otherPrices == null) return true; if (version != otherPrices.version) return true; if (priceInnerRecordHandling != otherPrices.priceInnerRecordHandling) return true; if (priceIndex.size() != otherPrices.priceIndex.size()) return true; for (Entry entry : priceIndex.entrySet()) { final PriceContract otherPrice = otherPrices.getPrice(entry.getKey()).orElse(null); if (otherPrice == null || entry.getValue().differsFrom(otherPrice)) { return true; } } return false; } @Override public String toString() { if (pricesAvailable()) { final Collection prices = getPrices(); return "selects " + priceInnerRecordHandling + " from: " + ( prices.isEmpty() ? "no price" : prices .stream() .map(Object::toString) .collect(Collectors.joining(", ")) ); } else { return "entity has no prices"; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy