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

io.evitadb.api.requestResponse.data.structure.AssociatedData Maven / Gradle / Ivy

There is a newer version: 2024.10.0
Show newest version
/*
 *
 *                         _ _        ____  ____
 *               _____   _(_) |_ __ _|  _ \| __ )
 *              / _ \ \ / / | __/ _` | | | |  _ \
 *             |  __/\ V /| | || (_| | |_| | |_) |
 *              \___| \_/ |_|\__\__,_|____/|____/
 *
 *   Copyright (c) 2023
 *
 *   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.AssociatedDataNotFoundException;
import io.evitadb.api.exception.ContextMissingException;
import io.evitadb.api.query.require.AssociatedDataContent;
import io.evitadb.api.requestResponse.data.AssociatedDataContract;
import io.evitadb.api.requestResponse.data.AssociatedDataEditor.AssociatedDataBuilder;
import io.evitadb.api.requestResponse.schema.AssociatedDataSchemaContract;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.dataType.data.ComplexDataObjectConverter;
import io.evitadb.exception.EvitaInvalidUsageException;
import io.evitadb.utils.Assert;
import io.evitadb.utils.ReflectionLookup;
import lombok.EqualsAndHashCode;

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.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.evitadb.utils.CollectionUtils.createLinkedHashMap;
import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;

/**
 * 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.
 *
 * 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 AssociatedDataBuilder}.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
 */
@EqualsAndHashCode
@Immutable
@ThreadSafe
public class AssociatedData implements AssociatedDataContract {
	@Serial private static final long serialVersionUID = 4916435515883999950L;
	/**
	 * Definition of the entity schema.
	 */
	final EntitySchemaContract entitySchema;
	/**
	 * Contains locale insensitive associatedData values - simple key → value association map.
	 */
	final Map associatedDataValues;
	/**
	 * Contains associatedData definition that is built up along way with associatedData adding or it may be directly filled
	 * in from the engine when entity with associated data is loaded from persistent storage.
	 */
	final Map associatedDataTypes;
	/**
	 * Optimization that ensures that expensive associatedData name resolving happens only once.
	 */
	private Set associatedDataNames;
	/**
	 * Optimization that ensures that expensive associatedData name resolving happens only once.
	 */
	private Set associatedDataKeys;
	/**
	 * Optimization that ensures that expensive associatedData name resolving happens only once.
	 */
	private List filteredAssociatedDataValues;
	/**
	 * Optimization that ensures that expensive associatedData locale resolving happens only once.
	 */
	private Set associatedDataLocales;

	/**
	 * Constructor should be used only when associated data are loaded from persistent storage.
	 * Constructor is meant to be internal to the Evita engine.
	 */
	public AssociatedData(
		@Nonnull EntitySchemaContract entitySchema,
		@Nonnull Collection associatedDataValues,
		@Nonnull Map associatedDataTypes
	) {
		this.entitySchema = entitySchema;
		this.associatedDataValues = createLinkedHashMap(associatedDataValues.size());
		for (AssociatedDataValue associatedDataValue : associatedDataValues) {
			this.associatedDataValues.put(associatedDataValue.key(), associatedDataValue);
		}
		this.associatedDataTypes = associatedDataTypes;
	}

	/**
	 * Constructor should be used only when associated data are loaded from persistent storage.
	 * Constructor is meant to be internal to the Evita engine.
	 */
	public AssociatedData(
		@Nonnull EntitySchemaContract entitySchema,
		@Nullable Stream associatedDataValues
	) {
		this.entitySchema = entitySchema;
		this.associatedDataValues = ofNullable(associatedDataValues)
			.map(it -> it.collect(
					Collectors.toMap(
						AssociatedDataValue::key,
						Function.identity(),
						(attributeValue, attributeValue2) -> {
							throw new EvitaInvalidUsageException("Duplicated attribute " + attributeValue.key() + "!");
						},
						() -> (Map) new LinkedHashMap()
					)
				)
			)
			.orElse(Collections.emptyMap());
		this.associatedDataTypes = entitySchema.getAssociatedData();
	}

	/**
	 * Constructor should be used only when associated data are reconstructed in APIs.
	 */
	public AssociatedData(
		@Nonnull EntitySchemaContract entitySchema,
		@Nonnull LinkedHashMap associatedDataValues
	) {
		this.entitySchema = entitySchema;
		this.associatedDataValues = associatedDataValues;
		this.associatedDataTypes = entitySchema.getAssociatedData();
	}

	/**
	 * Constructor should be used when new associated data are added to the entity.
	 *
	 * @param entitySchema entity schema
	 */
	public AssociatedData(@Nonnull EntitySchemaContract entitySchema) {
		this.entitySchema = entitySchema;
		this.associatedDataValues = Collections.emptyMap();
		this.associatedDataTypes = entitySchema.getAssociatedData();
	}

	@Override
	public boolean associatedDataAvailable() {
		return true;
	}

	@Override
	public boolean associatedDataAvailable(@Nonnull Locale locale) {
		return true;
	}

	@Override
	public boolean associatedDataAvailable(@Nonnull String associatedDataName) {
		return true;
	}

	@Override
	public boolean associatedDataAvailable(@Nonnull String associatedDataName, @Nonnull Locale locale) {
		return true;
	}

	@Override
	@Nullable
	public  T getAssociatedData(@Nonnull String associatedDataName) {
		final AssociatedDataSchemaContract associatedDataSchema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		Assert.isTrue(
			!associatedDataSchema.isLocalized(),
			() -> ContextMissingException.localeForAssociatedDataContextMissing(associatedDataName)
		);
		//noinspection unchecked
		return (T) ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)))
			.map(AssociatedDataContract.AssociatedDataValue::value)
			.orElse(null);
	}

	@Nullable
	@Override
	public  T getAssociatedData(@Nonnull String associatedDataName, @Nonnull Class dtoType, @Nonnull ReflectionLookup reflectionLookup) {
		final AssociatedDataSchemaContract associatedDataSchema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		Assert.isTrue(
			!associatedDataSchema.isLocalized(),
			() -> ContextMissingException.localeForAssociatedDataContextMissing(associatedDataName)
		);
		return ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)))
			.map(AssociatedDataContract.AssociatedDataValue::value)
			.map(it -> ComplexDataObjectConverter.getOriginalForm(it, dtoType, reflectionLookup))
			.orElse(null);
	}

	@Override
	@Nullable
	public  T[] getAssociatedDataArray(@Nonnull String associatedDataName) {
		final AssociatedDataSchemaContract associatedDataSchema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		Assert.isTrue(
			!associatedDataSchema.isLocalized(),
			() -> ContextMissingException.localeForAssociatedDataContextMissing(associatedDataName)
		);
		//noinspection unchecked
		return (T[]) ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)))
			.map(AssociatedDataContract.AssociatedDataValue::value)
			.orElse(null);
	}

	@Nonnull
	@Override
	public Optional getAssociatedDataValue(@Nonnull String associatedDataName) {
		final AssociatedDataSchemaContract associatedDataSchema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		if (associatedDataSchema.isLocalized()) {
			return empty();
		} else {
			return ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)));
		}
	}

	@Override
	@Nullable
	public  T getAssociatedData(@Nonnull String associatedDataName, @Nonnull Locale locale) {
		final AssociatedDataSchemaContract schema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		//noinspection unchecked
		return (T) (schema.isLocalized() ?
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale))) :
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName))))
			.map(AssociatedDataContract.AssociatedDataValue::value)
			.orElse(null);
	}

	@Nullable
	@Override
	public  T getAssociatedData(@Nonnull String associatedDataName, @Nonnull Locale locale, @Nonnull Class dtoType, @Nonnull ReflectionLookup reflectionLookup) {
		final AssociatedDataSchemaContract schema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		return (T) (schema.isLocalized() ?
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale))) :
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName))))
			.map(AssociatedDataContract.AssociatedDataValue::value)
			.map(it -> ComplexDataObjectConverter.getOriginalForm(it, dtoType, reflectionLookup))
			.orElse(null);
	}

	@Override
	@Nullable
	public  T[] getAssociatedDataArray(@Nonnull String associatedDataName, @Nonnull Locale locale) {
		final AssociatedDataSchemaContract schema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		//noinspection unchecked,ConstantConditions
		return (T[]) (schema.isLocalized() ?
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale))) :
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName))))
			.map(AssociatedDataContract.AssociatedDataValue::value)
			.orElse(null);
	}

	@Nonnull
	@Override
	public Optional getAssociatedDataValue(@Nonnull String associatedDataName, @Nonnull Locale locale) {
		final AssociatedDataSchemaContract schema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		return schema.isLocalized() ?
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale))) :
			ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)));
	}

	@Override
	@Nonnull
	public Optional getAssociatedDataSchema(@Nonnull String associatedDataName) {
		return ofNullable(associatedDataTypes.get(associatedDataName));
	}

	@Override
	@Nonnull
	public Set getAssociatedDataNames() {
		if (this.associatedDataNames == null) {
			this.associatedDataNames = this.associatedDataValues
				.values()
				.stream()
				.filter(ad -> ad.value() != null)
				.map(AssociatedDataValue::key)
				.map(AssociatedDataKey::associatedDataName)
				.filter(dataName -> associatedDataTypes.get(dataName) != null)
				.collect(Collectors.toSet());
		}
		return this.associatedDataNames;
	}

	@Nonnull
	@Override
	public Set getAssociatedDataKeys() {
		if (this.associatedDataKeys == null) {
			this.associatedDataKeys = this.associatedDataValues
				.values()
				.stream()
				.filter(ad -> ad.value() != null)
				.map(AssociatedDataValue::key)
				.filter(key -> associatedDataTypes.get(key.associatedDataName()) != null)
				.collect(Collectors.toUnmodifiableSet());
		}
		return this.associatedDataKeys;
	}

	/**
	 * Returns collection of all associated data of the entity.
	 */
	@Override
	@Nonnull
	public Collection getAssociatedDataValues() {
		if (this.filteredAssociatedDataValues == null) {
			this.filteredAssociatedDataValues = this.associatedDataValues
				.values()
				.stream()
				.filter(ad -> ad.value() != null)
				.filter(ad -> associatedDataTypes.get(ad.key().associatedDataName()) != null)
				.collect(Collectors.toList());
		}
		return filteredAssociatedDataValues;
	}

	@Nonnull
	@Override
	public Collection getAssociatedDataValues(@Nonnull String associatedDataName) {
		if (associatedDataTypes.get(associatedDataName) == null) {
			throw new AssociatedDataNotFoundException(associatedDataName, entitySchema);
		} else {
			return associatedDataValues
				.entrySet()
				.stream().filter(it -> associatedDataName.equals(it.getKey().associatedDataName()))
				.map(Entry::getValue)
				.collect(Collectors.toList());
		}
	}

	@Nonnull
	@Override
	public Set getAssociatedDataLocales() {
		if (this.associatedDataLocales == null) {
			this.associatedDataLocales = this.associatedDataValues
				.keySet()
				.stream()
				.map(AssociatedDataKey::locale)
				.filter(Objects::nonNull)
				.collect(Collectors.toSet());
		}
		return this.associatedDataLocales;
	}

	@Nonnull
	@Override
	public Optional getAssociatedDataValue(@Nonnull AssociatedDataKey associatedDataKey) {
		final String associatedDataName = associatedDataKey.associatedDataName();
		final AssociatedDataSchemaContract schema = ofNullable(associatedDataTypes.get(associatedDataName))
			.orElseThrow(() -> new AssociatedDataNotFoundException(associatedDataName, entitySchema));
		return schema.isLocalized() ?
			ofNullable(associatedDataValues.get(associatedDataKey)) :
			ofNullable(associatedDataValues.get(associatedDataKey.localized() ? new AssociatedDataKey(associatedDataName) : associatedDataKey));
	}

	/**
	 * Returns attribute by business key without checking if the attribute is defined in the schema.
	 * Method is part of PRIVATE API.
	 */
	@Nonnull
	public Optional getAssociatedDataValueWithoutSchemaCheck(@Nonnull AssociatedDataKey associatedDataKey) {
		return ofNullable(associatedDataValues.get(associatedDataKey))
			.or(() -> associatedDataKey.localized() ? ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataKey.associatedDataName()))) : empty());
	}

	/**
	 * Returns true if there is no associated data set.
	 */
	public boolean isEmpty() {
		return this.associatedDataValues.isEmpty();
	}

	@Override
	public String toString() {
		return getAssociatedDataValues()
			.stream()
			.map(AssociatedDataValue::toString)
			.collect(Collectors.joining("; "));
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy