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

org.openmetadata.service.jdbi3.TopicRepository Maven / Gradle / Ivy

There is a newer version: 1.5.11
Show newest version
/*
 *  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.Include.ALL;
import static org.openmetadata.schema.type.Include.NON_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_TAGS;
import static org.openmetadata.service.Entity.populateEntityFieldTags;
import static org.openmetadata.service.resources.tags.TagLabelUtil.addDerivedTags;
import static org.openmetadata.service.resources.tags.TagLabelUtil.checkMutuallyExclusive;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.api.feed.ResolveTask;
import org.openmetadata.schema.entity.data.Topic;
import org.openmetadata.schema.entity.services.MessagingService;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Field;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.type.TaskType;
import org.openmetadata.schema.type.topic.CleanupPolicy;
import org.openmetadata.schema.type.topic.TopicSampleData;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.jdbi3.FeedRepository.TaskWorkflow;
import org.openmetadata.service.jdbi3.FeedRepository.ThreadContext;
import org.openmetadata.service.resources.feeds.MessageParser.EntityLink;
import org.openmetadata.service.resources.topics.TopicResource;
import org.openmetadata.service.security.mask.PIIMasker;
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;

public class TopicRepository extends EntityRepository {

  public TopicRepository() {
    super(
        TopicResource.COLLECTION_PATH,
        Entity.TOPIC,
        Topic.class,
        Entity.getCollectionDAO().topicDAO(),
        "",
        "");
    supportsSearch = true;
  }

  @Override
  public void setFullyQualifiedName(Topic topic) {
    topic.setFullyQualifiedName(
        FullyQualifiedName.add(topic.getService().getFullyQualifiedName(), topic.getName()));
    if (topic.getMessageSchema() != null) {
      setFieldFQN(topic.getFullyQualifiedName(), topic.getMessageSchema().getSchemaFields());
    }
  }

  @Override
  public void prepare(Topic topic, boolean update) {
    MessagingService messagingService = Entity.getEntity(topic.getService(), "", ALL);
    topic.setService(messagingService.getEntityReference());
    topic.setServiceType(messagingService.getServiceType());
  }

  @Override
  public void storeEntity(Topic topic, boolean update) {
    // Relationships and fields such as service are derived and not stored as part of json
    EntityReference service = topic.getService();
    topic.withService(null);

    // Don't store fields tags as JSON but build it on the fly based on relationships
    List fieldsWithTags = null;
    if (topic.getMessageSchema() != null) {
      fieldsWithTags = topic.getMessageSchema().getSchemaFields();
      topic.getMessageSchema().setSchemaFields(cloneWithoutTags(fieldsWithTags));
      topic.getMessageSchema().getSchemaFields().forEach(field -> field.setTags(null));
    }

    store(topic, update);

    // Restore the relationships
    if (fieldsWithTags != null) {
      topic.getMessageSchema().withSchemaFields(fieldsWithTags);
    }
    topic.withService(service);
  }

  @Override
  public void storeRelationships(Topic topic) {
    addServiceRelationship(topic, topic.getService());
  }

  @Override
  public void setFields(Topic topic, Fields fields) {
    topic.setService(getContainer(topic.getId()));
    if (topic.getMessageSchema() != null) {
      populateEntityFieldTags(
          entityType,
          topic.getMessageSchema().getSchemaFields(),
          topic.getFullyQualifiedName(),
          fields.contains(FIELD_TAGS));
    }
  }

  @Override
  public void clearFields(Topic topic, Fields fields) {
    /* Nothing to do */
  }

  @Override
  public TopicUpdater getUpdater(Topic original, Topic updated, Operation operation) {
    return new TopicUpdater(original, updated, operation);
  }

  public Topic getSampleData(UUID topicId, boolean authorizePII) {
    // Validate the request content
    Topic topic = find(topicId, NON_DELETED);

    TopicSampleData sampleData =
        JsonUtils.readValue(
            daoCollection.entityExtensionDAO().getExtension(topic.getId(), "topic.sampleData"),
            TopicSampleData.class);
    topic.setSampleData(sampleData);
    setFieldsInternal(topic, Fields.EMPTY_FIELDS);

    // Set the fields tags. Will be used to mask the sample data
    if (!authorizePII) {
      populateEntityFieldTags(
          entityType,
          topic.getMessageSchema().getSchemaFields(),
          topic.getFullyQualifiedName(),
          true);
      topic.setTags(getTags(topic));
      return PIIMasker.getSampleData(topic);
    }

    return topic;
  }

  public Topic addSampleData(UUID topicId, TopicSampleData sampleData) {
    // Validate the request content
    Topic topic = daoCollection.topicDAO().findEntityById(topicId);

    daoCollection
        .entityExtensionDAO()
        .insert(topicId, "topic.sampleData", "topicSampleData", JsonUtils.pojoToJson(sampleData));
    setFieldsInternal(topic, Fields.EMPTY_FIELDS);
    return topic.withSampleData(sampleData);
  }

  private void setFieldFQN(String parentFQN, List fields) {
    fields.forEach(
        c -> {
          String fieldFqn = FullyQualifiedName.add(parentFQN, c.getName());
          c.setFullyQualifiedName(fieldFqn);
          if (c.getChildren() != null) {
            setFieldFQN(fieldFqn, c.getChildren());
          }
        });
  }

  List cloneWithoutTags(List fields) {
    if (nullOrEmpty(fields)) {
      return fields;
    }
    List copy = new ArrayList<>();
    fields.forEach(f -> copy.add(cloneWithoutTags(f)));
    return copy;
  }

  private Field cloneWithoutTags(Field field) {
    List children = cloneWithoutTags(field.getChildren());
    return new Field()
        .withDescription(field.getDescription())
        .withName(field.getName())
        .withDisplayName(field.getDisplayName())
        .withFullyQualifiedName(field.getFullyQualifiedName())
        .withDataType(field.getDataType())
        .withDataTypeDisplay(field.getDataTypeDisplay())
        .withChildren(children);
  }

  private void validateSchemaFieldTags(List fields) {
    // Add field level tags by adding tag to field relationship
    for (Field field : fields) {
      validateTags(field.getTags());
      field.setTags(addDerivedTags(field.getTags()));
      checkMutuallyExclusive(field.getTags());
      if (field.getChildren() != null) {
        validateSchemaFieldTags(field.getChildren());
      }
    }
  }

  private void applyTags(List fields) {
    // Add field level tags by adding tag to field relationship
    for (Field field : fields) {
      applyTags(field.getTags(), field.getFullyQualifiedName());
      if (field.getChildren() != null) {
        applyTags(field.getChildren());
      }
    }
  }

  @Override
  public void applyTags(Topic topic) {
    // Add table level tags by adding tag to table relationship
    super.applyTags(topic);
    if (topic.getMessageSchema() != null) {
      applyTags(topic.getMessageSchema().getSchemaFields());
    }
  }

  @Override
  public EntityInterface getParentEntity(Topic entity, String fields) {
    return Entity.getEntity(entity.getService(), fields, Include.ALL);
  }

  @Override
  public void validateTags(Topic entity) {
    super.validateTags(entity);
    if (entity.getMessageSchema() != null) {
      validateSchemaFieldTags(entity.getMessageSchema().getSchemaFields());
    }
  }

  @Override
  public List getAllTags(EntityInterface entity) {
    List allTags = new ArrayList<>();
    Topic topic = (Topic) entity;
    EntityUtil.mergeTags(allTags, topic.getTags());
    List schemaFields =
        topic.getMessageSchema() != null ? topic.getMessageSchema().getSchemaFields() : null;
    for (Field schemaField : listOrEmpty(schemaFields)) {
      EntityUtil.mergeTags(allTags, schemaField.getTags());
    }
    return allTags;
  }

  @Override
  public TaskWorkflow getTaskWorkflow(ThreadContext threadContext) {
    validateTaskThread(threadContext);
    EntityLink entityLink = threadContext.getAbout();
    if (entityLink.getFieldName().equals("messageSchema")) {
      TaskType taskType = threadContext.getThread().getTask().getType();
      if (EntityUtil.isDescriptionTask(taskType)) {
        return new MessageSchemaDescriptionWorkflow(threadContext);
      } else if (EntityUtil.isTagTask(taskType)) {
        return new MessageSchemaTagWorkflow(threadContext);
      } else {
        throw new IllegalArgumentException(String.format("Invalid task type %s", taskType));
      }
    }
    return super.getTaskWorkflow(threadContext);
  }

  static class MessageSchemaDescriptionWorkflow extends DescriptionTaskWorkflow {
    private final Field schemaField;

    MessageSchemaDescriptionWorkflow(ThreadContext threadContext) {
      super(threadContext);
      schemaField =
          getSchemaField(
              (Topic) threadContext.getAboutEntity(), threadContext.getAbout().getArrayFieldName());
    }

    @Override
    public EntityInterface performTask(String user, ResolveTask resolveTask) {
      schemaField.setDescription(resolveTask.getNewValue());
      return threadContext.getAboutEntity();
    }
  }

  static class MessageSchemaTagWorkflow extends TagTaskWorkflow {
    private final Field schemaField;

    MessageSchemaTagWorkflow(ThreadContext threadContext) {
      super(threadContext);
      schemaField =
          getSchemaField(
              (Topic) threadContext.getAboutEntity(), threadContext.getAbout().getArrayFieldName());
    }

    @Override
    public EntityInterface performTask(String user, ResolveTask resolveTask) {
      List tags = JsonUtils.readObjects(resolveTask.getNewValue(), TagLabel.class);
      schemaField.setTags(tags);
      return threadContext.getAboutEntity();
    }
  }

  private static Field getSchemaField(Topic topic, String schemaName) {
    String childrenSchemaName = "";
    if (schemaName.contains(".")) {
      String fieldNameWithoutQuotes = schemaName.substring(1, schemaName.length() - 1);
      schemaName = fieldNameWithoutQuotes.substring(0, fieldNameWithoutQuotes.indexOf("."));
      childrenSchemaName =
          fieldNameWithoutQuotes.substring(fieldNameWithoutQuotes.lastIndexOf(".") + 1);
    }
    Field schemaField = null;
    for (Field field : topic.getMessageSchema().getSchemaFields()) {
      if (field.getName().equals(schemaName)) {
        schemaField = field;
        break;
      }
    }
    if (!"".equals(childrenSchemaName) && schemaField != null) {
      schemaField = getChildSchemaField(schemaField.getChildren(), childrenSchemaName);
    }
    if (schemaField == null) {
      throw new IllegalArgumentException(
          CatalogExceptionMessage.invalidFieldName("schema", schemaName));
    }
    return schemaField;
  }

  private static Field getChildSchemaField(List fields, String childrenSchemaName) {
    Field childrenSchemaField = null;
    for (Field field : fields) {
      if (field.getName().equals(childrenSchemaName)) {
        childrenSchemaField = field;
        break;
      }
    }
    if (childrenSchemaField == null) {
      for (Field field : fields) {
        if (field.getChildren() != null) {
          childrenSchemaField = getChildSchemaField(field.getChildren(), childrenSchemaName);
          if (childrenSchemaField != null) {
            break;
          }
        }
      }
    }
    return childrenSchemaField;
  }

  public static Set getAllFieldTags(Field field) {
    Set tags = new HashSet<>();
    if (!listOrEmpty(field.getTags()).isEmpty()) {
      tags.addAll(field.getTags());
    }
    for (Field c : listOrEmpty(field.getChildren())) {
      tags.addAll(getAllFieldTags(c));
    }
    return tags;
  }

  public class TopicUpdater extends EntityUpdater {
    public static final String FIELD_DATA_TYPE_DISPLAY = "dataTypeDisplay";

    public TopicUpdater(Topic original, Topic updated, Operation operation) {
      super(original, updated, operation);
    }

    @Transaction
    @Override
    public void entitySpecificUpdate() {
      recordChange(
          "maximumMessageSize", original.getMaximumMessageSize(), updated.getMaximumMessageSize());
      recordChange(
          "minimumInSyncReplicas",
          original.getMinimumInSyncReplicas(),
          updated.getMinimumInSyncReplicas());
      // Partitions is a required field. Cannot be null.
      if (updated.getPartitions() != null) {
        recordChange("partitions", original.getPartitions(), updated.getPartitions());
      }
      recordChange(
          "replicationFactor", original.getReplicationFactor(), updated.getReplicationFactor());
      recordChange("retentionTime", original.getRetentionTime(), updated.getRetentionTime());
      recordChange("retentionSize", original.getRetentionSize(), updated.getRetentionSize());
      if (updated.getMessageSchema() != null) {
        recordChange(
            "messageSchema.schemaText",
            original.getMessageSchema() == null
                ? null
                : original.getMessageSchema().getSchemaText(),
            updated.getMessageSchema().getSchemaText());
        recordChange(
            "messageSchema.schemaType",
            original.getMessageSchema() == null
                ? null
                : original.getMessageSchema().getSchemaType(),
            updated.getMessageSchema().getSchemaType());
        updateSchemaFields(
            "messageSchema.schemaFields",
            original.getMessageSchema() == null
                ? new ArrayList<>()
                : listOrEmpty(original.getMessageSchema().getSchemaFields()),
            listOrEmpty(updated.getMessageSchema().getSchemaFields()),
            EntityUtil.schemaFieldMatch);
      }
      recordChange("topicConfig", original.getTopicConfig(), updated.getTopicConfig());
      updateCleanupPolicies(original, updated);
      recordChange("sourceUrl", original.getSourceUrl(), updated.getSourceUrl());
      recordChange("sourceHash", original.getSourceHash(), updated.getSourceHash());
    }

    private void updateCleanupPolicies(Topic original, Topic updated) {
      List added = new ArrayList<>();
      List deleted = new ArrayList<>();
      recordListChange(
          "cleanupPolicies",
          original.getCleanupPolicies(),
          updated.getCleanupPolicies(),
          added,
          deleted,
          CleanupPolicy::equals);
    }

    private void updateSchemaFields(
        String fieldName,
        List origFields,
        List updatedFields,
        BiPredicate fieldMatch) {
      List deletedFields = new ArrayList<>();
      List addedFields = new ArrayList<>();
      recordListChange(
          fieldName, origFields, updatedFields, addedFields, deletedFields, fieldMatch);
      // carry forward tags and description if deletedFields matches added field
      Map addedFieldMap =
          addedFields.stream().collect(Collectors.toMap(Field::getName, Function.identity()));

      for (Field deleted : deletedFields) {
        if (addedFieldMap.containsKey(deleted.getName())) {
          Field addedField = addedFieldMap.get(deleted.getName());
          if (nullOrEmpty(addedField.getDescription()) && nullOrEmpty(deleted.getDescription())) {
            addedField.setDescription(deleted.getDescription());
          }
          if (nullOrEmpty(addedField.getTags()) && nullOrEmpty(deleted.getTags())) {
            addedField.setTags(deleted.getTags());
          }
        }
      }

      // Delete tags related to deleted fields
      deletedFields.forEach(
          deleted ->
              daoCollection.tagUsageDAO().deleteTagsByTarget(deleted.getFullyQualifiedName()));

      // Add tags related to newly added fields
      for (Field added : addedFields) {
        applyTags(added.getTags(), added.getFullyQualifiedName());
      }

      // Carry forward the user generated metadata from existing fields to new fields
      for (Field updated : updatedFields) {
        // Find stored field matching name, data type and ordinal position
        Field stored =
            origFields.stream().filter(c -> fieldMatch.test(c, updated)).findAny().orElse(null);
        if (stored == null) { // New field added
          continue;
        }

        updateFieldDescription(stored, updated);
        updateFieldDataTypeDisplay(stored, updated);
        updateFieldDisplayName(stored, updated);
        updateTags(
            stored.getFullyQualifiedName(),
            EntityUtil.getFieldName(fieldName, updated.getName(), FIELD_TAGS),
            stored.getTags(),
            updated.getTags());

        if (updated.getChildren() != null && stored.getChildren() != null) {
          String childrenFieldName = EntityUtil.getFieldName(fieldName, updated.getName());
          updateSchemaFields(
              childrenFieldName,
              listOrEmpty(stored.getChildren()),
              listOrEmpty(updated.getChildren()),
              fieldMatch);
        }
      }

      majorVersionChange = majorVersionChange || !deletedFields.isEmpty();
    }

    private void updateFieldDescription(Field origField, Field updatedField) {
      if (operation.isPut() && !nullOrEmpty(origField.getDescription()) && updatedByBot()) {
        // Revert the non-empty field description if being updated by a bot
        updatedField.setDescription(origField.getDescription());
        return;
      }
      String field = EntityUtil.getSchemaField(original, origField, FIELD_DESCRIPTION);
      recordChange(field, origField.getDescription(), updatedField.getDescription());
    }

    private void updateFieldDisplayName(Field origField, Field updatedField) {
      if (operation.isPut() && !nullOrEmpty(origField.getDescription()) && updatedByBot()) {
        // Revert the non-empty field description if being updated by a bot
        updatedField.setDisplayName(origField.getDisplayName());
        return;
      }
      String field = EntityUtil.getSchemaField(original, origField, FIELD_DISPLAY_NAME);
      recordChange(field, origField.getDisplayName(), updatedField.getDisplayName());
    }

    private void updateFieldDataTypeDisplay(Field origField, Field updatedField) {
      if (operation.isPut() && !nullOrEmpty(origField.getDataTypeDisplay()) && updatedByBot()) {
        // Revert the non-empty field dataTypeDisplay if being updated by a bot
        updatedField.setDataTypeDisplay(origField.getDataTypeDisplay());
        return;
      }
      String field = EntityUtil.getSchemaField(original, origField, FIELD_DATA_TYPE_DISPLAY);
      recordChange(field, origField.getDataTypeDisplay(), updatedField.getDataTypeDisplay());
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy