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

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

import io.evitadb.api.exception.AttributeNotFoundException;
import io.evitadb.api.exception.ContextMissingException;
import io.evitadb.api.query.Query;
import io.evitadb.api.query.QueryUtils;
import io.evitadb.api.requestResponse.data.structure.Attributes;
import io.evitadb.api.requestResponse.data.structure.Entity;
import io.evitadb.api.requestResponse.schema.AttributeSchemaContract;
import io.evitadb.dataType.EvitaDataTypes;
import io.evitadb.utils.Assert;
import io.evitadb.utils.ComparatorUtils;
import io.evitadb.utils.MemoryMeasuringConstants;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serial;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This interface prescribes a set of methods that must be implemented by the object, that maintains set of attributes.
 *
 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
 */
public interface AttributesContract extends Serializable, AttributesAvailabilityChecker {

	/**
	 * Returns true if single attribute differs between first and second instance.
	 */
	static  boolean anyAttributeDifferBetween(@Nonnull AttributesContract first, @Nonnull AttributesContract second) {
		final Collection thisValues = first.attributesAvailable() ? first.getAttributeValues() : Collections.emptyList();
		final Collection otherValues = second.attributesAvailable() ? second.getAttributeValues() : Collections.emptyList();

		if (thisValues.size() != otherValues.size()) {
			return true;
		} else {
			return thisValues
				.stream()
				.anyMatch(it -> {
					final Serializable thisValue = it.value();
					final AttributeKey key = it.key();
					final AttributeValue other = second.getAttributeValue(key.attributeName(), key.locale())
						.orElse(null);
					if (other == null) {
						return true;
					} else {
						final Serializable otherValue = other.value();
						return it.dropped() != other.dropped() || QueryUtils.valueDiffers(thisValue, otherValue);
					}
				});
		}
	}

	/**
	 * Returns value associated with the key or null when the attribute is missing.
	 * This method variant differs from {@link #getAttribute(String, Class)} in the sense that it relies on Java local
	 * variable type inference. It's shorted, but it can't be used on every place. You may safely use in this context:
	 *
	 * ``` java
	 * // when the name attribute is of type String
	 * final String name = entity.getAttribute("name");
	 * ```
	 *
	 * @throws ClassCastException         when attribute is of different type than expected
	 * @throws ContextMissingException    when attribute is localized and entity is not related to any {@link Query} or
	 *                                    the query lacks locale identifier, or the attribute was not fetched at all
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 */
	@Nullable
	 T getAttribute(@Nonnull String attributeName)
		throws ContextMissingException, AttributeNotFoundException;

	/**
	 * Returns value associated with the key or null when the attribute is missing.
	 * This method variant differs from {@link #getAttribute(String)} in the sense that it specifies expected returned
	 * type as an input argument and doesn't rely on Java local variable type inference. This approach needs to be
	 * used in {@link Optional} or {@link Stream} contexts.
	 *
	 * @throws ClassCastException         when attribute is of different type than expected
	 * @throws ContextMissingException    when attribute is localized and entity is not related to any {@link Query} or
	 *                                    the query lacks locale identifier, or the attribute was not fetched at all
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 */
	@Nullable
	default  T getAttribute(@Nonnull String attributeName, @Nonnull Class expectedClass)
		throws ContextMissingException, AttributeNotFoundException {
		return getAttribute(attributeName);
	}

	/**
	 * Returns array of values associated with the key or null when the attribute is missing.
	 * This method variant differs from {@link #getAttribute(String, Class)} in the sense that it relies on Java local
	 * variable type inference. It's shorted, but it can't be used on every place.
	 *
	 * @throws ClassCastException         when attribute is of different type than expected or is not an array
	 * @throws ContextMissingException    when attribute is localized and entity is not related to any {@link Query} or
	 *                                    the query lacks locale identifier, or the attribute was not fetched at all
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 */
	@Nullable
	 T[] getAttributeArray(@Nonnull String attributeName)
		throws ContextMissingException, AttributeNotFoundException;

	/**
	 * Returns array of values associated with the key or null when the attribute is missing.
	 * This method variant differs from {@link #getAttribute(String)} in the sense that it specifies expected returned
	 * type as an input argument and doesn't rely on Java local variable type inference.
	 *
	 * @throws ClassCastException         when attribute is of different type than expected or is not an array
	 * @throws ContextMissingException    when attribute is localized and entity is not related to any {@link Query} or
	 *                                    the query lacks locale identifier, or the attribute was not fetched at all
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 */
	@Nullable
	default  T[] getAttributeArray(@Nonnull String attributeName, @Nonnull Class expectedType)
		throws ContextMissingException, AttributeNotFoundException {
		return getAttributeArray(attributeName);
	}

	/**
	 * Returns value associated with the key or null when the attribute is missing. This method doesn't throw any
	 * {@link ContextMissingException}, but instead it returns an empty value even for localized attributes.
	 *
	 * Method returns wrapper dto for the attribute that contains information about the attribute version and state.
	 *
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when attribute is localized and entity is not related to any {@link Query} or
	 *                                    the query lacks locale identifier, or the attribute was not fetched at all
	 */
	@Nonnull
	Optional getAttributeValue(@Nonnull String attributeName) throws AttributeNotFoundException;

	/**
	 * Returns value associated with the key or null when the attribute is missing.
	 * When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this
	 * method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not.
	 *
	 * @throws ClassCastException         when attribute is of different type than expected
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when the query lacks locale identifier, or the attribute was not fetched at all
	 */
	@Nullable
	 T getAttribute(@Nonnull String attributeName, @Nonnull Locale locale)
		throws AttributeNotFoundException;

	/**
	 * Returns value associated with the key or null when the attribute is missing.
	 * When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this
	 * method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not.
	 * This method variant differs from {@link #getAttribute(String)} in the sense that it specifies expected returned
	 * type as an input argument and doesn't rely on Java local variable type inference.
	 *
	 * @throws ClassCastException         when attribute is of different type than expected
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when the query lacks locale identifier, or the attribute was not fetched at all
	 */
	default  T getAttribute(@Nonnull String attributeName, @Nonnull Locale locale, @Nonnull Class expectedType)
		throws AttributeNotFoundException {
		return getAttribute(attributeName, locale);
	}

	/**
	 * Returns array of values associated with the key or null when the attribute is missing.
	 * When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this
	 * method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not.
	 *
	 * @throws ClassCastException         when attribute is of different type than expected or is not an array
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when the query lacks locale identifier, or the attribute was not fetched at all
	 */
	@Nullable
	 T[] getAttributeArray(@Nonnull String attributeName, @Nonnull Locale locale)
		throws AttributeNotFoundException;

	/**
	 * Returns array of values associated with the key or null when the attribute is missing.
	 * When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this
	 * method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not.
	 * This method variant differs from {@link #getAttribute(String)} in the sense that it specifies expected returned
	 * type as an input argument and doesn't rely on Java local variable type inference.
	 *
	 * @throws ClassCastException         when attribute is of different type than expected or is not an array
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when the query lacks locale identifier, or the attribute was not fetched at all
	 */
	default  T[] getAttributeArray(@Nonnull String attributeName, @Nonnull Locale locale, @Nonnull Class expectedType)
		throws AttributeNotFoundException {
		return getAttributeArray(attributeName, locale);
	}

	/**
	 * Returns array of values associated with the key or null when the attribute is missing.
	 * When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this
	 * method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not.
	 *
	 * Method returns wrapper dto for the attribute that contains information about the attribute version and state.
	 *
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when the query lacks locale identifier, or the attribute was not fetched at all
	 */
	@Nonnull
	Optional getAttributeValue(@Nonnull String attributeName, @Nonnull Locale locale)
		throws AttributeNotFoundException;

	/**
	 * Returns definition for the attribute of specified name.
	 */
	@Nonnull
	Optional getAttributeSchema(@Nonnull String attributeName);

	/**
	 * Returns set of all attribute names registered in this attribute set. The result set is not limited to the set
	 * of currently fetched attributes.
	 */
	@Nonnull
	Set getAttributeNames();

	/**
	 * Returns set of all keys (combination of attribute name and locale) registered in this attribute set.
	 */
	@Nonnull
	Set getAttributeKeys();

	/**
	 * Returns array of values associated with the key or null when the attribute is missing.
	 * When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this
	 * method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not.
	 *
	 * Method returns wrapper dto for the attribute that contains information about the attribute version and state.
	 *
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when attribute is localized and entity is not related to any {@link Query} or
	 *                                    the query lacks locale identifier, or the attribute was not fetched at all
	 */
	@Nonnull
	Optional getAttributeValue(@Nonnull AttributeKey attributeKey);

	/**
	 * Returns collection of all values present in this object.
	 */
	@Nonnull
	Collection getAttributeValues();


	/**
	 * Returns collection of all values of `attributeName` present in this object. This method has usually sense
	 * only when there is attribute in multiple localizations.
	 *
	 * @throws AttributeNotFoundException when attribute is not defined in the schema
	 * @throws ContextMissingException    when attribute is localized and entity is not related to any {@link Query} or
	 *                                    the query lacks locale identifier, or the attribute was not fetched at all
	 */
	@Nonnull
	Collection getAttributeValues(@Nonnull String attributeName);

	/**
	 * Method returns set of all locales used in the localized attributes. The result set is not limited to the set
	 * of currently fetched attributes.
	 */
	@Nonnull
	Set getAttributeLocales();

	/**
	 * Inner implementation used in {@link Attributes} to represent a proper key in hash map.
	 *
	 * @param attributeName unique name of the attribute. Case-sensitive. Distinguishes one associated data item from
	 *                      another within single entity instance.
	 * @param locale        contains locale in case the attribute is locale specific (i.e. {@link AttributeSchemaContract#isLocalized()}
	 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
	 */
	record AttributeKey(
		@Nonnull String attributeName,
		@Nullable Locale locale

	) implements Serializable, Comparable {
		@Serial private static final long serialVersionUID = -8516513307116598241L;

		/**
		 * Constructor for the locale specific attribute.
		 */
		public AttributeKey {
			Assert.notNull(attributeName, "Attribute name cannot be null!");
		}

		/**
		 * Construction for the locale agnostics attribute.
		 */
		public AttributeKey(@Nonnull String attributeName) {
			this(attributeName, null);
		}

		/**
		 * Returns true if attribute is localized.
		 */
		public boolean localized() {
			return locale != null;
		}

		@Override
		public int compareTo(AttributeKey o) {
			return ComparatorUtils.compareLocale(locale, o.locale, () -> attributeName.compareTo(o.attributeName));
		}

		/**
		 * Method returns gross estimation of the in-memory size of this instance. The estimation is expected not to be
		 * a precise one. Please use constants from {@link MemoryMeasuringConstants} for size computation.
		 */
		public int estimateSize() {
			return MemoryMeasuringConstants.OBJECT_HEADER_SIZE +
				// attribute name
				MemoryMeasuringConstants.REFERENCE_SIZE + MemoryMeasuringConstants.computeStringSize(attributeName) +
				// locale
				MemoryMeasuringConstants.REFERENCE_SIZE;
		}

		@Override
		public String toString() {
			return attributeName + (locale == null ? "" : ":" + locale);
		}
	}

	/**
	 * Represents single attribute og the {@link Entity}. AttributeValue serves as wrapper for the attribute value
	 * that also carries current version of the value for the sake of optimistic locking and the locale (in case attribute
	 * is localized).
	 *
	 * @param version contains version of this object and gets increased with any attribute update. Allows to execute
	 *                optimistic locking i.e. avoiding parallel modifications.
	 * @param key     uniquely identifies the attribute value among other attributes in the same entity instance.
	 * @param value   contains the current value of the attribute
	 * @param dropped contains TRUE if attribute was dropped - i.e. removed. Such attributes are not removed (unless
	 *                tidying process does it), but are lying among other attributes with tombstone flag. Dropped
	 *                attributes can be overwritten by a new value continuing with the versioning where it was stopped
	 *                for the last time.
	 * @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
	 */
	record AttributeValue(
		int version,
		@Nonnull AttributeKey key,
		@Nullable Serializable value,
		boolean dropped
	) implements Versioned, Droppable, Serializable, Comparable, ContentComparator {
		@Serial private static final long serialVersionUID = -5387437940533059959L;

		/**
		 * Method can be used for sorted arrays binary searches but doesn't represent any valid attribute value.
		 */
		public static AttributeValue createEmptyComparableAttributeValue(@Nonnull AttributeKey attributeKey) {
			return new AttributeValue(attributeKey);
		}

		public AttributeValue(@Nonnull AttributeValue baseAttribute, @Nonnull Serializable replacedValue) {
			this(baseAttribute.version, baseAttribute.key, replacedValue, baseAttribute.dropped);
		}

		private AttributeValue(@Nonnull AttributeKey attributeKey) {
			this(1, attributeKey, null, false);
		}

		public AttributeValue(@Nonnull AttributeKey attributeKey, @Nonnull Serializable value) {
			this(1, attributeKey, value, false);
		}

		public AttributeValue(int version, @Nonnull AttributeKey attributeKey, @Nonnull Serializable value) {
			this(version, attributeKey, value, false);
		}

		public AttributeValue(int version, @Nonnull AttributeKey key, @Nonnull Serializable value, boolean dropped) {
			this.version = version;
			this.key = key;
			this.value = value;
			this.dropped = dropped;
		}

		@Override
		public int compareTo(AttributeValue o) {
			return key.compareTo(o.key);
		}

		/**
		 * Method returns gross estimation of the in-memory size of this instance. The estimation is expected not to be
		 * a precise one. Please use constants from {@link MemoryMeasuringConstants} for size computation.
		 */
		public int estimateSize() {
			return MemoryMeasuringConstants.OBJECT_HEADER_SIZE +
				// version
				MemoryMeasuringConstants.INT_SIZE +
				// dropped
				MemoryMeasuringConstants.BYTE_SIZE +
				// key
				key.estimateSize() +
				// value size estimate
				MemoryMeasuringConstants.REFERENCE_SIZE + (value == null ? 0 : EvitaDataTypes.estimateSize(value));
		}

		/**
		 * Returns true if this attribute differs in key factors from the passed attribute.
		 */
		@Override
		public boolean differsFrom(@Nullable AttributeValue otherAttributeValue) {
			if (otherAttributeValue == null) return true;
			if (!Objects.equals(key, otherAttributeValue.key)) return true;
			if (!Objects.equals(value, otherAttributeValue.value)) return true;
			return dropped != otherAttributeValue.dropped;
		}

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

			AttributeValue that = (AttributeValue) o;

			if (version != that.version) return false;
			return key.equals(that.key);
		}

		public int hashCode() {
			int result = version;
			result = 31 * result + key.hashCode();
			return result;
		}

		@Override
		public String toString() {
			return (dropped ? "❌ " : "") +
				"\uD83D\uDD11 " + key.attributeName() + " " +
				(key.locale() == null ? "" : "(" + key.locale() + ")") +
				": " +
				(
					value instanceof Object[] ?
						("[" + Arrays.stream((Object[]) value).map(Object::toString).collect(Collectors.joining(",")) + "]") :
						value
				);
		}
	}
}