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

io.evitadb.api.requestResponse.data.structure.InitialAssociatedDataBuilder 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.InvalidDataTypeMutationException;
import io.evitadb.api.exception.InvalidMutationException;
import io.evitadb.api.requestResponse.data.AssociatedDataEditor.AssociatedDataBuilder;
import io.evitadb.api.requestResponse.data.mutation.associatedData.AssociatedDataMutation;
import io.evitadb.api.requestResponse.schema.AssociatedDataSchemaContract;
import io.evitadb.api.requestResponse.schema.EntitySchemaContract;
import io.evitadb.api.requestResponse.schema.EvolutionMode;
import io.evitadb.dataType.data.ComplexDataObjectConverter;
import io.evitadb.utils.ArrayUtils;
import io.evitadb.utils.Assert;
import io.evitadb.utils.ReflectionLookup;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
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 java.util.Optional.empty;
import static java.util.Optional.ofNullable;

/**
 * Class supports intermediate mutable object that allows {@link AssociatedData} container rebuilding.
 * Due to performance reasons (see {@link DirectWriteOrOperationLog} microbenchmark) there is special implementation
 * for the situation when entity is newly created. In this case we know everything is new and we don't need to closely
 * monitor the changes so this can speed things up.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
 */
class InitialAssociatedDataBuilder implements AssociatedDataBuilder {
	@Serial private static final long serialVersionUID = 7714436064799237939L;
	/**
	 * Entity schema if available.
	 */
	private final EntitySchemaContract entitySchema;
	/**
	 * Contains locale insensitive associatedData values - simple key → value association map.
	 */
	private final Map associatedDataValues;

	/**
	 * AssociatedDataBuilder constructor that will be used for building brand new {@link AssociatedData} container.
	 */
	InitialAssociatedDataBuilder(@Nonnull EntitySchemaContract entitySchema) {
		this.entitySchema = entitySchema;
		this.associatedDataValues = new HashMap<>();
	}

	@Nonnull
	@Override
	public Optional getAssociatedDataSchema(@Nonnull String associatedDataName) {
		return this.entitySchema.getAssociatedData(associatedDataName);
	}

	@Nonnull
	@Override
	public Set getAssociatedDataNames() {
		return this.associatedDataValues
				.keySet()
				.stream()
				.map(AssociatedDataKey::associatedDataName)
				.collect(Collectors.toSet());
	}

	@Nonnull
	@Override
	public Set getAssociatedDataKeys() {
		return this.associatedDataValues.keySet();
	}

	@Nonnull
	@Override
	public Collection getAssociatedDataValues() {
		return this.associatedDataValues.values();
	}

	@Override
	@Nonnull
	public AssociatedDataBuilder removeAssociatedData(@Nonnull String associatedDataName) {
		final AssociatedDataKey associatedDataKey = new AssociatedDataKey(associatedDataName);
		associatedDataValues.remove(associatedDataKey);
		return this;
	}

	@Override
	@Nonnull
	public  AssociatedDataBuilder setAssociatedData(@Nonnull String associatedDataName, @Nullable T associatedDataValue) {
		if (associatedDataValue == null || associatedDataValue instanceof Object[] arr && ArrayUtils.isEmpty(arr)) {
			return removeAssociatedData(associatedDataName);
		} else {
			final Serializable valueToStore = ComplexDataObjectConverter.getSerializableForm(associatedDataValue);
			final AssociatedDataKey associatedDataKey = new AssociatedDataKey(associatedDataName);
			verifyAssociatedDataIsInSchemaAndTypeMatch(entitySchema, associatedDataName, valueToStore.getClass());
			associatedDataValues.put(associatedDataKey, new AssociatedDataValue(associatedDataKey, valueToStore));
			return this;
		}
	}

	@Override
	@Nonnull
	public  AssociatedDataBuilder setAssociatedData(@Nonnull String associatedDataName, @Nonnull T[] associatedDataValue) {
		final Serializable valueToStore = ComplexDataObjectConverter.getSerializableForm(associatedDataValue);
		final AssociatedDataKey associatedDataKey = new AssociatedDataKey(associatedDataName);
		verifyAssociatedDataIsInSchemaAndTypeMatch(entitySchema, associatedDataName, valueToStore.getClass());
		associatedDataValues.put(associatedDataKey, new AssociatedDataValue(associatedDataKey, valueToStore));
		return this;
	}

	@Override
	@Nullable
	public  T getAssociatedData(@Nonnull String associatedDataName) {
		//noinspection unchecked
		return (T) ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)))
				.map(AssociatedDataValue::value)
				.orElse(null);
	}

	@Nullable
	@Override
	public  T getAssociatedData(@Nonnull String associatedDataName, @Nonnull Class dtoType, @Nonnull ReflectionLookup reflectionLookup) {
		return ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)))
				.map(AssociatedDataValue::value)
				.map(it -> ComplexDataObjectConverter.getOriginalForm(it, dtoType, reflectionLookup))
				.orElse(null);
	}

	@Override
	@Nullable
	public  T[] getAssociatedDataArray(@Nonnull String associatedDataName) {
		//noinspection unchecked
		return (T[]) ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)))
			.map(AssociatedDataValue::value)
			.orElse(null);
	}

	@Nonnull
	@Override
	public Optional getAssociatedDataValue(@Nonnull String associatedDataName) {
		return ofNullable(associatedDataValues.get(new AssociatedDataKey(associatedDataName)));
	}

	@Nonnull
	@Override
	public Collection getAssociatedDataValues(@Nonnull String associatedDataName) {
		return associatedDataValues
			.entrySet()
			.stream()
			.filter(it -> associatedDataName.equals(it.getKey().associatedDataName()))
			.map(Entry::getValue)
			.collect(Collectors.toList());
	}

	/*
		LOCALIZED AssociatedDataS
	 */

	@Override
	@Nonnull
	public AssociatedDataBuilder removeAssociatedData(@Nonnull String associatedDataName, @Nonnull Locale locale) {
		final AssociatedDataKey associatedDataKey = new AssociatedDataKey(associatedDataName, locale);
		this.associatedDataValues.remove(associatedDataKey);
		return this;
	}

	@Override
	@Nonnull
	public  AssociatedDataBuilder setAssociatedData(@Nonnull String associatedDataName, @Nonnull Locale locale, @Nullable T associatedDataValue) {
		if (associatedDataValue == null || associatedDataValue instanceof Object[] arr && ArrayUtils.isEmpty(arr)) {
			return removeAssociatedData(associatedDataName, locale);
		} else {
			final Serializable valueToStore = ComplexDataObjectConverter.getSerializableForm(associatedDataValue);
			final AssociatedDataKey associatedDataKey = new AssociatedDataKey(associatedDataName, locale);
			verifyAssociatedDataIsInSchemaAndTypeMatch(entitySchema, associatedDataName, valueToStore.getClass(), locale);
			this.associatedDataValues.put(associatedDataKey, new AssociatedDataValue(associatedDataKey, valueToStore));
			return this;
		}
	}

	@Override
	@Nonnull
	public  AssociatedDataBuilder setAssociatedData(@Nonnull String associatedDataName, @Nonnull Locale locale, @Nullable T[] associatedDataValue) {
		if (associatedDataValue == null) {
			return removeAssociatedData(associatedDataName, locale);
		} else {
			final Serializable valueToStore = ComplexDataObjectConverter.getSerializableForm(associatedDataValue);
			final AssociatedDataKey associatedDataKey = new AssociatedDataKey(associatedDataName, locale);
			verifyAssociatedDataIsInSchemaAndTypeMatch(entitySchema, associatedDataName, valueToStore.getClass(), locale);
			this.associatedDataValues.put(associatedDataKey, new AssociatedDataValue(associatedDataKey, valueToStore));
			return this;
		}
	}

	@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, @Nonnull Locale locale) {
		//noinspection unchecked
		return (T) ofNullable(this.associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale)))
				.map(AssociatedDataValue::value)
				.orElse(null);
	}

	@Nullable
	@Override
	public  T getAssociatedData(@Nonnull String associatedDataName, @Nonnull Locale locale, @Nonnull Class dtoType, @Nonnull ReflectionLookup reflectionLookup) {
		return ofNullable(this.associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale)))
				.map(AssociatedDataValue::value)
				.map(it -> ComplexDataObjectConverter.getOriginalForm(it, dtoType, reflectionLookup))
				.orElse(null);
	}

	@Override
	@Nullable
	public  T[] getAssociatedDataArray(@Nonnull String associatedDataName, @Nonnull Locale locale) {
		//noinspection unchecked
		return (T[]) ofNullable(this.associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale)))
				.map(AssociatedDataValue::value)
				.orElse(null);
	}

	@Nonnull
	@Override
	public Optional getAssociatedDataValue(@Nonnull String associatedDataName, @Nonnull Locale locale) {
		return ofNullable(this.associatedDataValues.get(new AssociatedDataKey(associatedDataName, locale)));
	}

	@Nonnull
	@Override
	public Optional getAssociatedDataValue(@Nonnull AssociatedDataKey associatedDataKey) {
		return ofNullable(this.associatedDataValues.get(associatedDataKey))
			.or(() -> associatedDataKey.localized() ?
				ofNullable(this.associatedDataValues.get(new AssociatedDataKey(associatedDataKey.associatedDataName()))) :
				empty()
			);
	}

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

	@Nonnull
	@Override
	public AssociatedDataBuilder mutateAssociatedData(@Nonnull AssociatedDataMutation mutation) {
		throw new UnsupportedOperationException("You cannot apply mutation when entity is just being created!");
	}

	@Nonnull
	@Override
	public Stream buildChangeSet() {
		throw new UnsupportedOperationException("Initial entity creation doesn't support change monitoring - it has no sense.");
	}

	@Nonnull
	@Override
	public AssociatedData build() {
		// let's check whether there are compatible attributes
		final Map associatedDataTypes = this.associatedDataValues
			.values()
			.stream()
			.map(AssociatedDataBuilder::createImplicitSchema)
			.collect(
				Collectors.toMap(
					AssociatedDataSchemaContract::getName,
					Function.identity(),
					(associatedDataType, associatedDataType2) -> {
						Assert.isTrue(
							Objects.equals(associatedDataType, associatedDataType2),
							"Ambiguous situation - there are two associated data with the same name and different definition:\n" +
								associatedDataType + "\n" +
								associatedDataType2
						);
						return associatedDataType;
					}
				)
			);

		return new AssociatedData(
			this.entitySchema,
			this.associatedDataValues.values(),
			associatedDataTypes
		);
	}

	static void verifyAssociatedDataIsInSchemaAndTypeMatch(
		@Nonnull EntitySchemaContract entitySchema,
		@Nonnull String associatedDataName,
		@Nullable Class aClass
	) {
		verifyAssociatedDataIsInSchemaAndTypeMatch(
			entitySchema, associatedDataName, aClass, null,
			entitySchema.getAssociatedData(associatedDataName).orElse(null)
		);
	}

	static void verifyAssociatedDataIsInSchemaAndTypeMatch(
		@Nonnull EntitySchemaContract entitySchema,
		@Nonnull String associatedDataName,
		@Nullable Class aClass,
		@Nonnull Locale locale
	) {
		verifyAssociatedDataIsInSchemaAndTypeMatch(
			entitySchema, associatedDataName, aClass, locale,
			entitySchema.getAssociatedData(associatedDataName).orElse(null)
		);
	}


	static void verifyAssociatedDataIsInSchemaAndTypeMatch(
		@Nonnull EntitySchemaContract entitySchema,
		@Nonnull String associatedDataName,
		@Nullable Class aClass,
		@Nullable Locale locale,
		@Nullable AssociatedDataSchemaContract associatedDataSchema
	) {
		Assert.isTrue(
				associatedDataSchema != null || entitySchema.allows(EvolutionMode.ADDING_ASSOCIATED_DATA),
				() -> new InvalidMutationException(
						"AssociatedData " + associatedDataName + " is not configured in entity " + entitySchema.getName() +
								" schema and automatic evolution is not enabled for associated data!"
				)
		);
		if (associatedDataSchema != null) {
			if (aClass != null) {
				Assert.isTrue(
						associatedDataSchema.getType().isAssignableFrom(aClass),
						() -> new InvalidDataTypeMutationException(
								"AssociatedData " + associatedDataName + " accepts only type " + associatedDataSchema.getType().getName() +
										" - value type is different: " + aClass.getName() + "!",
								associatedDataSchema.getType(), aClass
						)
				);
			}
			if (locale == null) {
				Assert.isTrue(
						!associatedDataSchema.isLocalized(),
						() -> new InvalidMutationException(
								"AssociatedData " + associatedDataName + " is localized and doesn't accept non-localized associated data!"
						)
				);
			} else {
				Assert.isTrue(
						associatedDataSchema.isLocalized(),
						() -> new InvalidMutationException(
								"AssociatedData " + associatedDataName + " is not localized and doesn't accept localized associated data!"
						)
				);
				Assert.isTrue(
						entitySchema.supportsLocale(locale) || entitySchema.allows(EvolutionMode.ADDING_LOCALES),
						() -> new InvalidMutationException(
								"AssociatedData " + associatedDataName + " is localized, but schema doesn't support locale " + locale + "! " +
										"Supported locales are: " +
										entitySchema.getLocales().stream().map(Locale::toString).collect(Collectors.joining(", "))
						)
				);
			}
		} else if (locale != null) {
			// at least verify supported locale
			Assert.isTrue(
					entitySchema.supportsLocale(locale) || entitySchema.allows(EvolutionMode.ADDING_LOCALES),
					() -> new InvalidMutationException(
							"AssociatedData " + associatedDataName + " is localized, but schema doesn't support locale " + locale + "! " +
									"Supported locales are: " +
									entitySchema.getLocales().stream().map(Locale::toString).collect(Collectors.joining(", "))
					)
			);
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy