io.evitadb.api.requestResponse.extraResult.Hierarchy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of evita_api Show documentation
Show all versions of evita_api Show documentation
Module contains external API of the evitaDB.
/*
*
* _ _ ____ ____
* _____ _(_) |_ __ _| _ \| __ )
* / _ \ \ / / | __/ _` | | | | _ \
* | __/\ 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.extraResult;
import io.evitadb.api.query.filter.HierarchyWithin;
import io.evitadb.api.query.filter.HierarchyWithinRoot;
import io.evitadb.api.query.require.EmptyHierarchicalEntityBehaviour;
import io.evitadb.api.query.require.EntityContentRequire;
import io.evitadb.api.query.require.HierarchyOfSelf;
import io.evitadb.api.requestResponse.EvitaResponseExtraResult;
import io.evitadb.api.requestResponse.data.EntityClassifier;
import io.evitadb.api.requestResponse.data.SealedEntity;
import lombok.RequiredArgsConstructor;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import java.io.Serial;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static java.util.Optional.ofNullable;
/**
* This DTO contains hierarchical structure of entities either directly queried or referenced by the entities targeted
* by the query. It copies hierarchical structure of those entities and contains their identification or full body as
* well as information on cardinality of referencing entities.
*
* For example when we need to render menu for entire e-commerce site, but we want to take excluded subtrees into
* an account and also reflect the filtering conditions that may filter out dozens of products (and thus leading to
* empty categories) we can invoke following query:
*
*
* query(
* entities('PRODUCT'),
* filterBy(
* and(
* attributeEquals('visible', true),
* attributeInRange('valid', 2020-07-30T20:37:50+00:00),
* priceInCurrency('USD'),
* priceValidIn(2020-07-30T20:37:50+00:00),
* priceInPriceLists('vip', 'standard'),
* hierarchyWithinRoot('categories', excluding(entityPrimaryKeyInSet(3, 7)))
* )
* ),
* require(
* page(1, 20),
* hierarchyStatisticsOfReference('categories', entityFetch(attributeContentAll()))
* )
* )
*
*
* This query would return first page with 20 products (omitting hundreds of others on additional pages) but also
* returns a HierarchyStatistics in additional data. Statistics respect hierarchical constraints specified in the filter
* of the query. In our example sub-trees with ids 3 and 7 will be omitted from the statistics.
*
* This object may contain following structure:
*
*
* Electronics -> 1789
* TV -> 126
* LED -> 90
* CRT -> 36
* Washing machines -> 190
* Slim -> 40
* Standard -> 40
* With drier -> 23
* Top filling -> 42
* Smart -> 45
* Cell phones -> 350
* Audio / Video -> 230
* Printers -> 80
*
*
* The tree will contain category entities loaded with `attributes` instead the names you see in the example. The number
* after the arrow represents the count of the products that are referencing this category (either directly or some of
* its children).
*
* @author Jan Novotný ([email protected]), FG Forrest a.s. (c) 2021
* @see LevelInfo for the list of all available information for each hierarchical entity
*/
@RequiredArgsConstructor
@ThreadSafe
public class Hierarchy implements EvitaResponseExtraResult {
@Serial private static final long serialVersionUID = -5337743162562869243L;
/**
* Contains list of statistics for the single level (probably root or whatever is filtered by the query) of
* the queried hierarchy entity.
*/
private final Map> selfStatistics;
/**
* Index holds the statistics for particular references that target hierarchy entity types.
* Key is the identification of the reference name, value contains list of statistics for the single level (probably
* root or whatever is filtered by the query) of the hierarchy entity.
*/
private final Map>> referenceHierarchies;
/**
* Compares two lists of {@link LevelInfo} objects for equality.
*/
private static boolean notEquals(@Nonnull List stats, @Nonnull List otherStats) {
for (int i = 0; i < stats.size(); i++) {
final LevelInfo levelInfo = stats.get(i);
final LevelInfo otherLevelInfo = otherStats.get(i);
if (!Objects.equals(levelInfo, otherLevelInfo)) {
return true;
}
}
return false;
}
/**
* Method returns the cardinality statistics for the top most level of queried hierarchical entities.
* Level is either the root level if {@link HierarchyWithinRoot} query or no hierarchical filtering query
* was used at all. Or it's the level requested by {@link HierarchyWithin} query.
*/
@Nonnull
public Map> getSelfHierarchy() {
return ofNullable(selfStatistics).orElse(Collections.emptyMap());
}
/**
* Method returns the cardinality statistics for the top most level of queried hierarchical entities.
* Level is either the root level if {@link HierarchyWithinRoot} query or no hierarchical filtering query
* was used at all. Or it's the level requested by {@link HierarchyWithin} query.
*/
@Nonnull
public List getSelfHierarchy(@Nonnull String outputName) {
return ofNullable(selfStatistics)
.map(it -> it.get(outputName))
.orElse(Collections.emptyList());
}
/**
* Method returns the cardinality statistics for the top most level of referenced hierarchical entities.
* Level is either the root level if {@link HierarchyWithinRoot} query or no hierarchical filtering query
* was used at all. Or it's the level requested by {@link HierarchyWithin} query.
*/
@Nonnull
public List getReferenceHierarchy(@Nonnull String referenceName, @Nonnull String outputName) {
return ofNullable(referenceHierarchies.get(referenceName))
.map(it -> it.get(outputName))
.orElse(Collections.emptyList());
}
/**
* Returns statistics for reference of specified name.
*/
@Nonnull
public Map> getReferenceHierarchy(@Nonnull String referenceName) {
return ofNullable(referenceHierarchies.get(referenceName)).orElse(Collections.emptyMap());
}
/**
* Returns statistics for all references.
*/
@Nonnull
public Map>> getReferenceHierarchies() {
return referenceHierarchies;
}
@Override
public int hashCode() {
return Objects.hash(selfStatistics, referenceHierarchies);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Hierarchy that = (Hierarchy) o;
if (selfStatistics == null && that.selfStatistics != null && !that.selfStatistics.isEmpty()) {
return false;
} else if (selfStatistics != null && !selfStatistics.isEmpty() && that.selfStatistics == null) {
return false;
}
if (selfStatistics != null) {
for (Entry> entry : selfStatistics.entrySet()) {
final List stats = entry.getValue();
final List otherStats = that.selfStatistics.get(entry.getKey());
if (stats.size() != ofNullable(otherStats).map(List::size).orElse(0)) {
return false;
}
if (notEquals(stats, otherStats)) {
return false;
}
}
}
for (Entry>> statisticsEntry : referenceHierarchies.entrySet()) {
final Map> stats = statisticsEntry.getValue();
final Map> otherStats = that.referenceHierarchies.get(statisticsEntry.getKey());
if (stats.size() != ofNullable(otherStats).map(Map::size).orElse(0)) {
return false;
}
for (Entry> entry : stats.entrySet()) {
final List innerStats = entry.getValue();
final List innerOtherStats = otherStats.get(entry.getKey());
if (innerStats.size() != ofNullable(innerOtherStats).map(List::size).orElse(0)) {
return false;
}
if (notEquals(innerStats, innerOtherStats)) {
return false;
}
}
}
return true;
}
@Override
public String toString() {
final StringBuilder treeBuilder = new StringBuilder();
if (selfStatistics != null) {
for (Map.Entry> statsByOutputName : selfStatistics.entrySet()) {
treeBuilder.append(statsByOutputName.getKey()).append(System.lineSeparator());
for (LevelInfo levelInfo : statsByOutputName.getValue()) {
appendLevelInfoTreeString(treeBuilder, levelInfo, 1);
}
}
}
for (Entry>> statisticsEntry : referenceHierarchies.entrySet()) {
treeBuilder.append(statisticsEntry.getKey()).append(System.lineSeparator());
for (Map.Entry> statisticsByType : statisticsEntry.getValue().entrySet()) {
treeBuilder.append(" ").append(statisticsByType.getKey()).append(System.lineSeparator());
for (LevelInfo levelInfo : statisticsByType.getValue()) {
appendLevelInfoTreeString(treeBuilder, levelInfo, 2);
}
}
}
return treeBuilder.toString();
}
/**
* Creates string representation of subtree of passed level info
*
* @param treeBuilder string builder to which the string will be appended to
* @param levelInfo level info to render
* @param currentLevel level on which passed level info is being placed
*/
private void appendLevelInfoTreeString(@Nonnull StringBuilder treeBuilder, @Nonnull LevelInfo levelInfo, int currentLevel) {
treeBuilder.append(" ".repeat(currentLevel))
.append(levelInfo)
.append(System.lineSeparator());
for (LevelInfo child : levelInfo.children()) {
appendLevelInfoTreeString(treeBuilder, child, currentLevel + 1);
}
}
/**
* This DTO represents single hierarchical entity in the statistics tree. It contains identification of the entity,
* the cardinality of queried entities that refer to it and information about children level.
*
* @param entity Hierarchical entity identification - it may be {@link Integer} representing primary key of the entity if no
* {@link EntityContentRequire} requirements were passed within {@link HierarchyOfSelf}
* query, or it may be rich {@link SealedEntity} object if the richer requirements were specified.
* @param requested true in case the entity was filtered by {@link HierarchyWithin}
* @param queriedEntityCount Contains the number of queried entities that refer directly to this {@link #entity} or to any of its children
* entities.
* @param childrenCount Contains number of hierarchical entities that are referring to this {@link #entity} as its parent.
* The count will respect {@link EmptyHierarchicalEntityBehaviour} settings and will not
* count empty children in case {@link EmptyHierarchicalEntityBehaviour#REMOVE_EMPTY} is
* used for computation.
* @param children Contains hierarchy info of the entities that are subordinate (children) of this {@link #entity}.
*/
public record LevelInfo(
@Nonnull EntityClassifier entity,
boolean requested,
@Nullable Integer queriedEntityCount,
@Nullable Integer childrenCount,
@Nonnull List children
) {
public LevelInfo(@Nonnull LevelInfo levelInfo, @Nonnull List children) {
this(levelInfo.entity, levelInfo.requested, levelInfo.queriedEntityCount, levelInfo.childrenCount, children);
}
/**
* Returns the list of self and all children of this level info that match the passed predicate.
*
* @param predicate predicate to match
* @return stream of all children that match the predicate including self
*/
@Nonnull
public Stream collectAll(@Nonnull Predicate predicate) {
return Stream.concat(
Stream.of(this).filter(predicate),
children.stream().flatMap(it -> it.collectAll(predicate))
);
}
@Override
public String toString() {
if (queriedEntityCount == null && childrenCount == null) {
return entity + (requested ? " (requested)" : "");
} else {
return "[" + queriedEntityCount + ":" + childrenCount + "] " + entity + (requested ? " (requested)" : "");
}
}
}
}