org.openmetadata.service.jdbi3.EntityRepository Maven / Gradle / Ivy
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* 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 org.openmetadata.service.jdbi3;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.schema.type.EventType.ENTITY_CREATED;
import static org.openmetadata.schema.type.EventType.ENTITY_DELETED;
import static org.openmetadata.schema.type.EventType.ENTITY_FIELDS_CHANGED;
import static org.openmetadata.schema.type.EventType.ENTITY_NO_CHANGE;
import static org.openmetadata.schema.type.EventType.ENTITY_RESTORED;
import static org.openmetadata.schema.type.EventType.ENTITY_SOFT_DELETED;
import static org.openmetadata.schema.type.EventType.ENTITY_UPDATED;
import static org.openmetadata.schema.type.Include.ALL;
import static org.openmetadata.schema.type.Include.DELETED;
import static org.openmetadata.schema.type.Include.NON_DELETED;
import static org.openmetadata.schema.utils.EntityInterfaceUtil.quoteName;
import static org.openmetadata.service.Entity.ADMIN_USER_NAME;
import static org.openmetadata.service.Entity.DATA_PRODUCT;
import static org.openmetadata.service.Entity.DOMAIN;
import static org.openmetadata.service.Entity.FIELD_CHILDREN;
import static org.openmetadata.service.Entity.FIELD_DATA_PRODUCTS;
import static org.openmetadata.service.Entity.FIELD_DELETED;
import static org.openmetadata.service.Entity.FIELD_DESCRIPTION;
import static org.openmetadata.service.Entity.FIELD_DISPLAY_NAME;
import static org.openmetadata.service.Entity.FIELD_DOMAIN;
import static org.openmetadata.service.Entity.FIELD_EXPERTS;
import static org.openmetadata.service.Entity.FIELD_EXTENSION;
import static org.openmetadata.service.Entity.FIELD_FOLLOWERS;
import static org.openmetadata.service.Entity.FIELD_LIFE_CYCLE;
import static org.openmetadata.service.Entity.FIELD_OWNERS;
import static org.openmetadata.service.Entity.FIELD_REVIEWERS;
import static org.openmetadata.service.Entity.FIELD_STYLE;
import static org.openmetadata.service.Entity.FIELD_TAGS;
import static org.openmetadata.service.Entity.FIELD_VOTES;
import static org.openmetadata.service.Entity.TEAM;
import static org.openmetadata.service.Entity.USER;
import static org.openmetadata.service.Entity.getEntityByName;
import static org.openmetadata.service.Entity.getEntityFields;
import static org.openmetadata.service.exception.CatalogExceptionMessage.csvNotSupported;
import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound;
import static org.openmetadata.service.resources.tags.TagLabelUtil.addDerivedTags;
import static org.openmetadata.service.resources.tags.TagLabelUtil.checkMutuallyExclusive;
import static org.openmetadata.service.util.EntityUtil.compareTagLabel;
import static org.openmetadata.service.util.EntityUtil.entityReferenceMatch;
import static org.openmetadata.service.util.EntityUtil.fieldAdded;
import static org.openmetadata.service.util.EntityUtil.fieldDeleted;
import static org.openmetadata.service.util.EntityUtil.fieldUpdated;
import static org.openmetadata.service.util.EntityUtil.getColumnField;
import static org.openmetadata.service.util.EntityUtil.getEntityReferences;
import static org.openmetadata.service.util.EntityUtil.getExtensionField;
import static org.openmetadata.service.util.EntityUtil.mergedInheritedEntityRefs;
import static org.openmetadata.service.util.EntityUtil.nextMajorVersion;
import static org.openmetadata.service.util.EntityUtil.nextVersion;
import static org.openmetadata.service.util.EntityUtil.objectMatch;
import static org.openmetadata.service.util.EntityUtil.tagLabelMatch;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.ValidationMessage;
import java.io.IOException;
import java.net.URI;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
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.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.json.JsonPatch;
import javax.validation.constraints.NotNull;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.CreateEntity;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.api.VoteRequest;
import org.openmetadata.schema.api.VoteRequest.VoteType;
import org.openmetadata.schema.api.feed.ResolveTask;
import org.openmetadata.schema.api.teams.CreateTeam;
import org.openmetadata.schema.email.SmtpSettings;
import org.openmetadata.schema.entity.data.Table;
import org.openmetadata.schema.entity.feed.Suggestion;
import org.openmetadata.schema.entity.teams.Team;
import org.openmetadata.schema.entity.teams.User;
import org.openmetadata.schema.settings.SettingsType;
import org.openmetadata.schema.system.EntityError;
import org.openmetadata.schema.type.ApiStatus;
import org.openmetadata.schema.type.ChangeDescription;
import org.openmetadata.schema.type.ChangeEvent;
import org.openmetadata.schema.type.Column;
import org.openmetadata.schema.type.EntityHistory;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.EventType;
import org.openmetadata.schema.type.FieldChange;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.LifeCycle;
import org.openmetadata.schema.type.ProviderType;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.SuggestionType;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.type.TaskType;
import org.openmetadata.schema.type.ThreadType;
import org.openmetadata.schema.type.Votes;
import org.openmetadata.schema.type.api.BulkAssets;
import org.openmetadata.schema.type.api.BulkOperationResult;
import org.openmetadata.schema.type.api.BulkResponse;
import org.openmetadata.schema.type.csv.CsvImportResult;
import org.openmetadata.schema.utils.EntityInterfaceUtil;
import org.openmetadata.service.Entity;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.TypeRegistry;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.exception.UnhandledServerException;
import org.openmetadata.service.jdbi3.CollectionDAO.EntityRelationshipRecord;
import org.openmetadata.service.jdbi3.CollectionDAO.EntityVersionPair;
import org.openmetadata.service.jdbi3.CollectionDAO.ExtensionRecord;
import org.openmetadata.service.jdbi3.FeedRepository.TaskWorkflow;
import org.openmetadata.service.jdbi3.FeedRepository.ThreadContext;
import org.openmetadata.service.resources.settings.SettingsCache;
import org.openmetadata.service.resources.tags.TagLabelUtil;
import org.openmetadata.service.search.SearchClient;
import org.openmetadata.service.search.SearchListFilter;
import org.openmetadata.service.search.SearchRepository;
import org.openmetadata.service.search.SearchSortFilter;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.FullyQualifiedName;
import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.RestUtil;
import org.openmetadata.service.util.RestUtil.DeleteResponse;
import org.openmetadata.service.util.RestUtil.PatchResponse;
import org.openmetadata.service.util.RestUtil.PutResponse;
import org.openmetadata.service.util.ResultList;
/**
* This is the base class used by Entity Resources to perform READ and WRITE operations to the backend database to
* Create, Retrieve, Update, and Delete entities.
*
* An entity has two types of fields - `attributes` and `relationships`.
*
*
* - The `attributes` are the core properties of the entity, example - entity id, name, fullyQualifiedName, columns
* for a table, etc.
*
- The `relationships` are an associated between two entities, example - table belongs to a database, table has a
* tag, user owns a table, etc. All relationships are captured using {@code EntityReference}.
*
*
* Entities are stored as JSON documents in the database. Each entity is stored in a separate table and is accessed
* through a Data Access Object or DAO that corresponds to each of the entity. For example,
* table_entity is the database table used to store JSON docs corresponding to table entity and {@link
* org.openmetadata.service.jdbi3.CollectionDAO.TableDAO} is used as the DAO object to access the table_entity table.
* All DAO objects for an entity are available in {@code daoCollection}.
*
* Relationships between entity is stored in a separate table that captures the edge - fromEntity, toEntity, and the
* relationship name entity_relationship table and are supported by {@link
* org.openmetadata.service.jdbi3.CollectionDAO.EntityRelationshipDAO} DAO object.
*
* JSON document of an entity stores only required attributes of an entity. Some attributes such as
* href are not stored and are created on the fly.
*
* Json document of an entity does not store relationships. As an example, JSON document for table entity does
* not store the relationship database which is of type EntityReference. This is always retrieved from the
* relationship table when required to ensure, the data stored is efficiently and consistently, and relationship
* information does not become stale.
*/
@Slf4j
@Repository()
public abstract class EntityRepository {
public record EntityHistoryWithOffset(EntityHistory entityHistory, int nextOffset) {}
public static final LoadingCache, EntityInterface> CACHE_WITH_NAME =
CacheBuilder.newBuilder()
.maximumSize(5000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.recordStats()
.build(new EntityLoaderWithName());
public static final LoadingCache, EntityInterface> CACHE_WITH_ID =
CacheBuilder.newBuilder()
.maximumSize(5000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.recordStats()
.build(new EntityLoaderWithId());
private final String collectionPath;
private final Class entityClass;
@Getter protected final String entityType;
@Getter protected final EntityDAO dao;
@Getter protected final CollectionDAO daoCollection;
@Getter protected final SearchRepository searchRepository;
@Getter protected final Set allowedFields;
public final boolean supportsSoftDelete;
@Getter protected final boolean supportsTags;
@Getter protected final boolean supportsOwners;
@Getter protected final boolean supportsStyle;
@Getter protected final boolean supportsLifeCycle;
protected final boolean supportsFollower;
protected final boolean supportsExtension;
protected final boolean supportsVotes;
@Getter protected final boolean supportsDomain;
protected final boolean supportsDataProducts;
@Getter protected final boolean supportsReviewers;
@Getter protected final boolean supportsExperts;
protected boolean quoteFqn =
false; // Entity FQNS not hierarchical such user, teams, services need to be quoted
protected boolean renameAllowed = false; // Entity can be renamed
/** Fields that can be updated during PATCH operation */
@Getter private final Fields patchFields;
/** Fields that can be updated during PUT operation */
@Getter protected final Fields putFields;
protected boolean supportsSearch = false;
protected EntityRepository(
String collectionPath,
String entityType,
Class entityClass,
EntityDAO entityDAO,
String patchFields,
String putFields) {
this.collectionPath = collectionPath;
this.entityClass = entityClass;
allowedFields = getEntityFields(entityClass);
this.dao = entityDAO;
this.daoCollection = Entity.getCollectionDAO();
this.searchRepository = Entity.getSearchRepository();
this.entityType = entityType;
this.patchFields = getFields(patchFields);
this.putFields = getFields(putFields);
this.supportsTags = allowedFields.contains(FIELD_TAGS);
if (supportsTags) {
this.patchFields.addField(allowedFields, FIELD_TAGS);
this.putFields.addField(allowedFields, FIELD_TAGS);
}
this.supportsOwners = allowedFields.contains(FIELD_OWNERS);
if (supportsOwners) {
this.patchFields.addField(allowedFields, FIELD_OWNERS);
this.putFields.addField(allowedFields, FIELD_OWNERS);
}
this.supportsSoftDelete = allowedFields.contains(FIELD_DELETED);
this.supportsFollower = allowedFields.contains(FIELD_FOLLOWERS);
if (supportsFollower) {
this.patchFields.addField(allowedFields, FIELD_FOLLOWERS);
this.putFields.addField(allowedFields, FIELD_FOLLOWERS);
}
this.supportsExtension = allowedFields.contains(FIELD_EXTENSION);
if (supportsExtension) {
this.patchFields.addField(allowedFields, FIELD_EXTENSION);
this.putFields.addField(allowedFields, FIELD_EXTENSION);
}
this.supportsVotes = allowedFields.contains(FIELD_VOTES);
if (supportsVotes) {
this.patchFields.addField(allowedFields, FIELD_VOTES);
this.putFields.addField(allowedFields, FIELD_VOTES);
}
this.supportsDomain = allowedFields.contains(FIELD_DOMAIN);
if (supportsDomain) {
this.patchFields.addField(allowedFields, FIELD_DOMAIN);
this.putFields.addField(allowedFields, FIELD_DOMAIN);
}
this.supportsReviewers = allowedFields.contains(FIELD_REVIEWERS);
if (supportsReviewers) {
this.patchFields.addField(allowedFields, FIELD_REVIEWERS);
this.putFields.addField(allowedFields, FIELD_REVIEWERS);
}
this.supportsExperts = allowedFields.contains(FIELD_EXPERTS);
if (supportsExperts) {
this.patchFields.addField(allowedFields, FIELD_EXPERTS);
this.putFields.addField(allowedFields, FIELD_EXPERTS);
}
this.supportsDataProducts = allowedFields.contains(FIELD_DATA_PRODUCTS);
if (supportsDataProducts) {
this.patchFields.addField(allowedFields, FIELD_DATA_PRODUCTS);
this.putFields.addField(allowedFields, FIELD_DATA_PRODUCTS);
}
this.supportsStyle = allowedFields.contains(FIELD_STYLE);
if (supportsStyle) {
this.patchFields.addField(allowedFields, FIELD_STYLE);
this.putFields.addField(allowedFields, FIELD_STYLE);
}
this.supportsLifeCycle = allowedFields.contains(FIELD_LIFE_CYCLE);
if (supportsLifeCycle) {
this.patchFields.addField(allowedFields, FIELD_LIFE_CYCLE);
this.putFields.addField(allowedFields, FIELD_LIFE_CYCLE);
}
Entity.registerEntity(entityClass, entityType, this);
}
/**
* Set the requested fields in an entity. This is used for requesting specific fields in the object during GET
* operations. It is also used during PUT and PATCH operations to set up fields that can be updated.
*/
protected abstract void setFields(T entity, Fields fields);
/**
* Set the requested fields in an entity. This is used for requesting specific fields in the object during GET
* operations. It is also used during PUT and PATCH operations to set up fields that can be updated.
*/
protected abstract void clearFields(T entity, Fields fields);
/**
* This method is used for validating an entity to be created during POST, PUT, and PATCH operations and prepare the
* entity with all the required attributes and relationships.
*
* The implementation of this method must perform the following:
*
*
* - Prepare the values for attributes that are not required in the request but can be derived on the server side.
* Example - >FullyQualifiedNames of an entity can be derived from the hierarchy that an entity belongs
* to .
*
- Validate all the attributes of an entity.
*
- Validate all the relationships of an entity. As an example - during table creation, relationships such
* as Tags, Owner, Databasea table belongs to are validated. During validation additional
* information that is not required in the create/update request are set up in the corresponding relationship
* fields.
*
*
* At the end of this operation, entity is expected to be valid and fully constructed with all the fields that will be
* sent as payload in the POST, PUT, and PATCH operations response.
*
* @see TableRepository#prepare(Table, boolean) for an example implementation
*/
protected abstract void prepare(T entity, boolean update);
/**
* An entity is stored in the backend database as JSON document. The JSON includes some attributes of the entity and
* does not include attributes such as href. The relationship fields of an entity is never stored in the JSON
* document. It is always reconstructed based on relationship edges from the backend database.
*
* As an example, when table entity is stored, the attributes such as href and the relationships such as
* owners, database, and tags are set to null. These attributes are restored back after the JSON
* document is stored to be sent as response.
*
* @see TableRepository#storeEntity(Table, boolean) for an example implementation
*/
protected abstract void storeEntity(T entity, boolean update);
/**
* This method is called to store all the relationships of an entity. It is expected that all relationships are
* already validated and completely setup before this method is called and no validation of relationships is required.
*
* @see TableRepository#storeRelationships(Table) for an example implementation
*/
protected abstract void storeRelationships(T entity);
/**
* This method is called to set inherited fields that an entity inherits from its parent.
*
* @see TableRepository#setInheritedFields(Table, Fields) for an example implementation
*/
@SuppressWarnings("unused")
protected void setInheritedFields(T entity, Fields fields) {
EntityInterface parent = supportsDomain ? getParentEntity(entity, "domain") : null;
if (parent != null) {
inheritDomain(entity, fields, parent);
}
}
protected final void addServiceRelationship(T entity, EntityReference service) {
if (service != null) {
addRelationship(
service.getId(), entity.getId(), service.getType(), entityType, Relationship.CONTAINS);
}
}
/**
* PATCH operations can't overwrite certain fields, such as entity ID, fullyQualifiedNames etc. Instead of throwing an
* error, we take lenient approach of ignoring the user error and restore those attributes based on what is already
* stored in the original entity.
*/
protected void restorePatchAttributes(T original, T updated) {
updated.setId(original.getId());
updated.setName(renameAllowed ? updated.getName() : original.getName());
updated.setFullyQualifiedName(original.getFullyQualifiedName());
updated.setChangeDescription(original.getChangeDescription());
}
/**
* This function updates the Elasticsearch indexes wherever the specific entity is present.
* It is typically invoked when there are changes in the entity that might affect its indexing in Elasticsearch.
* The function ensures that the indexes are kept up-to-date with the latest state of the entity across all relevant Elasticsearch indexes.
*/
protected void entityRelationshipReindex(T original, T updated) {
// Logic override by the child class to update the indexes
}
/** Set fullyQualifiedName of an entity */
public void setFullyQualifiedName(T entity) {
entity.setFullyQualifiedName(quoteName(entity.getName()));
}
/**
* Initialize data from json files if seed data does not exist in corresponding tables. Seed data is stored under
* openmetadata-service/src/main/resources/json/data/{entityType}
*
* This method needs to be explicitly called, typically from initialize method. See {@link
* org.openmetadata.service.resources.teams.RoleResource#initialize(OpenMetadataApplicationConfig)}
*/
public final void initSeedDataFromResources() throws IOException {
List entities = getEntitiesFromSeedData();
for (T entity : entities) {
initializeEntity(entity);
}
}
public final List getEntitiesFromSeedData() throws IOException {
List entitiesFromSeedData = new ArrayList<>();
if (entityType.equals(Entity.DOCUMENT)) {
SmtpSettings emailConfig =
SettingsCache.getSetting(SettingsType.EMAIL_CONFIGURATION, SmtpSettings.class);
switch (emailConfig.getTemplates()) {
case COLLATE -> {
entitiesFromSeedData.addAll(
getEntitiesFromSeedData(
String.format(".*json/data/%s/emailTemplates/collate/.*\\.json$", entityType)));
}
default -> {
entitiesFromSeedData.addAll(
getEntitiesFromSeedData(
String.format(
".*json/data/%s/emailTemplates/openmetadata/.*\\.json$", entityType)));
}
}
entitiesFromSeedData.addAll(
getEntitiesFromSeedData(String.format(".*json/data/%s/docs/.*\\.json$", entityType)));
return entitiesFromSeedData;
}
return getEntitiesFromSeedData(String.format(".*json/data/%s/.*\\.json$", entityType));
}
public final List getEntitiesFromSeedData(String path) throws IOException {
return getEntitiesFromSeedData(entityType, path, entityClass);
}
public static List getEntitiesFromSeedData(String entityType, String path, Class clazz)
throws IOException {
List entities = new ArrayList<>();
List jsonDataFiles = EntityUtil.getJsonDataResources(path);
jsonDataFiles.forEach(
jsonDataFile -> {
try {
String json =
CommonUtil.getResourceAsStream(
EntityRepository.class.getClassLoader(), jsonDataFile);
json = json.replace("", Entity.SEPARATOR);
entities.add(JsonUtils.readValue(json, clazz));
} catch (Exception e) {
LOG.warn("Failed to initialize the {} from file {}", entityType, jsonDataFile, e);
}
});
return entities;
}
/** Initialize a given entity if it does not exist. */
@Transaction
public final void initializeEntity(T entity) {
T existingEntity = findByNameOrNull(entity.getFullyQualifiedName(), ALL);
if (existingEntity != null) {
LOG.debug("{} {} is already initialized", entityType, entity.getFullyQualifiedName());
return;
}
LOG.debug("{} {} is not initialized", entityType, entity.getFullyQualifiedName());
entity.setUpdatedBy(ADMIN_USER_NAME);
entity.setUpdatedAt(System.currentTimeMillis());
entity.setId(UUID.randomUUID());
create(null, entity);
LOG.debug("Created a new {} {}", entityType, entity.getFullyQualifiedName());
}
public final T copy(T entity, CreateEntity request, String updatedBy) {
List owners = validateOwners(request.getOwners());
EntityReference domain = validateDomain(request.getDomain());
validateReviewers(request.getReviewers());
entity.setId(UUID.randomUUID());
entity.setName(request.getName());
entity.setDisplayName(request.getDisplayName());
entity.setDescription(request.getDescription());
entity.setOwners(owners);
entity.setDomain(domain);
entity.setTags(request.getTags());
entity.setDataProducts(getEntityReferences(Entity.DATA_PRODUCT, request.getDataProducts()));
entity.setLifeCycle(request.getLifeCycle());
entity.setExtension(request.getExtension());
entity.setUpdatedBy(updatedBy);
entity.setUpdatedAt(System.currentTimeMillis());
entity.setReviewers(request.getReviewers());
return entity;
}
protected EntityUpdater getUpdater(T original, T updated, Operation operation) {
return new EntityUpdater(original, updated, operation);
}
public final T get(UriInfo uriInfo, UUID id, Fields fields) {
return get(uriInfo, id, fields, NON_DELETED, false);
}
/** Used for getting an entity with a set of requested fields */
public final T get(UriInfo uriInfo, UUID id, Fields fields, Include include, boolean fromCache) {
if (!fromCache) {
// Clear the cache and always get the entity from the database to ensure read-after-write
// consistency
CACHE_WITH_ID.invalidate(new ImmutablePair<>(entityType, id));
}
// Find the entity from the cache. Set all the fields that are not already set
T entity = find(id, include);
setFieldsInternal(entity, fields);
setInheritedFields(entity, fields);
// Clone the entity from the cache and reset all the fields that are not already set
// Cloning is necessary to ensure different threads making a call to this method don't
// overwrite the fields of the entity being returned
T entityClone = JsonUtils.deepCopy(entity, entityClass);
clearFieldsInternal(entityClone, fields);
return withHref(uriInfo, entityClone);
}
/** getReference is used for getting the entity references from the entity in the cache. */
public final EntityReference getReference(UUID id, Include include)
throws EntityNotFoundException {
return find(id, include).getEntityReference();
}
/**
* Find method is used for getting an entity only with core fields stored as JSON without any relational fields set
*/
public final T find(UUID id, Include include) throws EntityNotFoundException {
return find(id, include, true);
}
public final T find(UUID id, Include include, boolean fromCache) throws EntityNotFoundException {
try {
if (!fromCache) {
CACHE_WITH_ID.invalidate(new ImmutablePair<>(entityType, id));
}
@SuppressWarnings("unchecked")
T entity = (T) CACHE_WITH_ID.get(new ImmutablePair<>(entityType, id));
if (include == NON_DELETED && Boolean.TRUE.equals(entity.getDeleted())
|| include == DELETED && !Boolean.TRUE.equals(entity.getDeleted())) {
throw new EntityNotFoundException(entityNotFound(entityType, id));
}
return entity;
} catch (ExecutionException | UncheckedExecutionException e) {
throw new EntityNotFoundException(entityNotFound(entityType, id));
}
}
public T getByName(UriInfo uriInfo, String fqn, Fields fields) {
return getByName(uriInfo, fqn, fields, NON_DELETED, false);
}
public final T getByName(
UriInfo uriInfo, String fqn, Fields fields, Include include, boolean fromCache) {
fqn = quoteFqn ? EntityInterfaceUtil.quoteName(fqn) : fqn;
if (!fromCache) {
// Clear the cache and always get the entity from the database to ensure read-after-write
// consistency
CACHE_WITH_NAME.invalidate(new ImmutablePair<>(entityType, fqn));
}
// Find the entity from the cache. Set all the fields that are not already set
T entity = findByName(fqn, include);
setFieldsInternal(entity, fields);
setInheritedFields(entity, fields);
// Clone the entity from the cache and reset all the fields that are not already set
// Cloning is necessary to ensure different threads making a call to this method don't
// overwrite the fields of the entity being returned
T entityClone = JsonUtils.deepCopy(entity, entityClass);
clearFieldsInternal(entityClone, fields);
return withHref(uriInfo, entityClone);
}
public final EntityReference getReferenceByName(String fqn, Include include) {
fqn = quoteFqn ? EntityInterfaceUtil.quoteName(fqn) : fqn;
return findByName(fqn, include).getEntityReference();
}
public final T findByNameOrNull(String fqn, Include include) {
try {
return findByName(fqn, include);
} catch (EntityNotFoundException e) {
return null;
}
}
/**
* Find method is used for getting an entity only with core fields stored as JSON without any relational fields set
*/
public final T findByName(String fqn, Include include) {
return findByName(fqn, include, true);
}
public final T findByName(String fqn, Include include, boolean fromCache) {
fqn = quoteFqn ? EntityInterfaceUtil.quoteName(fqn) : fqn;
try {
if (!fromCache) {
CACHE_WITH_NAME.invalidate(new ImmutablePair<>(entityType, fqn));
}
@SuppressWarnings("unchecked")
T entity = (T) CACHE_WITH_NAME.get(new ImmutablePair<>(entityType, fqn));
if (include == NON_DELETED && Boolean.TRUE.equals(entity.getDeleted())
|| include == DELETED && !Boolean.TRUE.equals(entity.getDeleted())) {
throw new EntityNotFoundException(entityNotFound(entityType, fqn));
}
return entity;
} catch (ExecutionException | UncheckedExecutionException e) {
throw new EntityNotFoundException(entityNotFound(entityType, fqn));
}
}
public final List listAll(Fields fields, ListFilter filter) {
// forward scrolling, if after == null then first page is being asked
List jsons = dao.listAfter(filter, Integer.MAX_VALUE, "", "");
List entities = new ArrayList<>();
for (String json : jsons) {
T entity = setFieldsInternal(JsonUtils.readValue(json, entityClass), fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
entities.add(entity);
}
return entities;
}
public ResultList listAfter(
UriInfo uriInfo, Fields fields, ListFilter filter, int limitParam, String after) {
int total = dao.listCount(filter);
List entities = new ArrayList<>();
if (limitParam > 0) {
// forward scrolling, if after == null then first page is being asked
Map cursorMap =
parseCursorMap(after == null ? "" : RestUtil.decodeCursor(after));
String afterName = FullyQualifiedName.unquoteName(cursorMap.get("name"));
String afterId = cursorMap.get("id");
List jsons = dao.listAfter(filter, limitParam + 1, afterName, afterId);
for (String json : jsons) {
T entity = setFieldsInternal(JsonUtils.readValue(json, entityClass), fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
entities.add(withHref(uriInfo, entity));
}
String beforeCursor;
String afterCursor = null;
beforeCursor = after == null ? null : getCursorValue(entities.get(0));
if (entities.size()
> limitParam) { // If extra result exists, then next page exists - return after cursor
entities.remove(limitParam);
afterCursor = getCursorValue(entities.get(limitParam - 1));
}
return getResultList(entities, beforeCursor, afterCursor, total);
} else {
// limit == 0 , return total count of entity.
return getResultList(entities, null, null, total);
}
}
public final ResultList listAfterWithSkipFailure(
UriInfo uriInfo, Fields fields, ListFilter filter, int limitParam, String after) {
List errors = new ArrayList<>();
List entities = new ArrayList<>();
int beforeOffset = Integer.parseInt(RestUtil.decodeCursor(after));
int currentOffset = beforeOffset;
int total = dao.listCount(filter);
if (limitParam > 0) {
List jsons = dao.listAfter(filter, limitParam, currentOffset);
for (String json : jsons) {
T parsedEntity = JsonUtils.readValue(json, entityClass);
try {
T entity = setFieldsInternal(parsedEntity, fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
entities.add(withHref(uriInfo, entity));
} catch (Exception e) {
clearFieldsInternal(parsedEntity, fields);
EntityError entityError =
new EntityError().withMessage(e.getMessage()).withEntity(parsedEntity);
errors.add(entityError);
LOG.error("[ListForIndexing] Failed for Entity : {}", entityError);
}
}
currentOffset = currentOffset + limitParam;
String newAfter = currentOffset > total ? null : String.valueOf(currentOffset);
return getResultList(entities, errors, String.valueOf(beforeOffset), newAfter, total);
} else {
// limit == 0 , return total count of entity.
return getResultList(entities, errors, null, null, total);
}
}
@SuppressWarnings("unchecked")
Map parseCursorMap(String param) {
Map cursorMap;
if (param == null) {
cursorMap = Map.of("name", null, "id", null);
} else if (nullOrEmpty(param)) {
cursorMap = Map.of("name", "", "id", "");
} else {
cursorMap = JsonUtils.readValue(param, Map.class);
}
return cursorMap;
}
public ResultList listBefore(
UriInfo uriInfo, Fields fields, ListFilter filter, int limitParam, String before) {
// Reverse scrolling - Get one extra result used for computing before cursor
Map cursorMap = parseCursorMap(RestUtil.decodeCursor(before));
String beforeName = FullyQualifiedName.unquoteName(cursorMap.get("name"));
String beforeId = cursorMap.get("id");
List jsons = dao.listBefore(filter, limitParam + 1, beforeName, beforeId);
List entities = new ArrayList<>();
for (String json : jsons) {
T entity = setFieldsInternal(JsonUtils.readValue(json, entityClass), fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
entities.add(withHref(uriInfo, entity));
}
int total = dao.listCount(filter);
String beforeCursor = null;
String afterCursor;
if (entities.size()
> limitParam) { // If extra result exists, then previous page exists - return before cursor
entities.remove(0);
beforeCursor = getCursorValue(entities.get(0));
}
afterCursor = getCursorValue(entities.get(entities.size() - 1));
return getResultList(entities, beforeCursor, afterCursor, total);
}
/**
* This method returns the cursor value for pagination.
* By default, it uses the entity's name. However, in cases where the name can be the same for different entities,
* it is recommended to override this method to use the (name,id) key instead.
* The id is always unique, which helps to avoid pagination issues caused by duplicate names and have unique ordering.
*/
public String getCursorValue(T entity) {
Map cursorMap =
Map.of("name", entity.getName(), "id", String.valueOf(entity.getId()));
return JsonUtils.pojoToJson(cursorMap);
}
public final T getVersion(UUID id, String version) {
Double requestedVersion = Double.parseDouble(version);
String extension = EntityUtil.getVersionExtension(entityType, requestedVersion);
// Get previous version from version history
String json = daoCollection.entityExtensionDAO().getExtension(id, extension);
if (json != null) {
return JsonUtils.readValue(json, entityClass);
}
// If requested the latest version, return it from current version of the entity
T entity = setFieldsInternal(find(id, ALL), putFields);
if (entity.getVersion().equals(requestedVersion)) {
return entity;
}
throw EntityNotFoundException.byMessage(
CatalogExceptionMessage.entityVersionNotFound(entityType, id, requestedVersion));
}
public final EntityHistoryWithOffset listVersionsWithOffset(UUID id, int limit, int offset) {
T latest = setFieldsInternal(find(id, ALL), putFields);
setInheritedFields(latest, putFields);
String extensionPrefix = EntityUtil.getVersionExtensionPrefix(entityType);
List records =
daoCollection
.entityExtensionDAO()
.getExtensionsWithOffset(id, extensionPrefix, limit, offset);
List oldVersions = new ArrayList<>();
records.forEach(r -> oldVersions.add(new EntityVersionPair(r)));
oldVersions.sort(EntityUtil.compareVersion.reversed());
final List