DataObjectDAO.java

package de.dlr.shepard.neo4Core.dao;

import de.dlr.shepard.neo4Core.entities.DataObject;
import de.dlr.shepard.neo4Core.entities.User;
import de.dlr.shepard.util.Constants;
import de.dlr.shepard.util.CypherQueryHelper;
import de.dlr.shepard.util.QueryParamHelper;
import jakarta.enterprise.context.RequestScoped;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.StreamSupport;

@RequestScoped
public class DataObjectDAO extends VersionableEntityDAO<DataObject> {

  @Override
  public Class<DataObject> getEntityType() {
    return DataObject.class;
  }

  /**
   * Searches the database for DataObjects.
   *
   * @param collectionId identifies the Collection
   * @param params       encapsulates possible parameters
   * @return a List of DataObjects
   */
  public List<DataObject> findByCollectionByNeo4jIds(long collectionId, QueryParamHelper params) {
    Map<String, Object> paramsMap = new HashMap<>();
    paramsMap.put("name", params.getName());
    if (params.hasPagination()) {
      paramsMap.put("offset", params.getPagination().getOffset());
      paramsMap.put("size", params.getPagination().getSize());
    }
    String match =
      "MATCH (c:Collection)-[hdo:has_dataobject]->" +
      CypherQueryHelper.getObjectPart("d", "DataObject", params.hasName());
    String where = " WHERE ID(c)=" + collectionId;

    if (params.hasParentId()) {
      if (params.getParentId() == -1) {
        where += " AND NOT EXISTS((d)<-[:has_child]-(:DataObject {deleted: FALSE}))";
      } else {
        match += "<-[:has_child]-(parent:DataObject {deleted: FALSE})";
        where += " AND ID(parent)=" + params.getParentId();
      }
    }

    if (params.hasPredecessorId()) {
      if (params.getPredecessorId() == -1) {
        where += " AND NOT EXISTS((d)<-[:has_successor]-(:DataObject {deleted: FALSE}))";
      } else {
        match += "<-[:has_successor]-(predecessor:DataObject {deleted: FALSE})";
        where += " AND ID(predecessor)=" + params.getPredecessorId();
      }
    }
    if (params.hasSuccessorId()) {
      if (params.getSuccessorId() == -1) {
        where += " AND NOT EXISTS((d)-[:has_successor]->(:DataObject {deleted: FALSE}))";
      } else {
        match += "-[:has_successor]->(successor:DataObject {deleted: FALSE})";
        where += " AND ID(successor)=" + params.getSuccessorId();
      }
    }

    String query = match + where + " WITH d";
    if (params.hasOrderByAttribute()) {
      query += " " + CypherQueryHelper.getOrderByPart("d", params.getOrderByAttribute(), params.getOrderDesc());
    }
    if (params.hasPagination()) {
      query += " " + CypherQueryHelper.getPaginationPart();
    }
    query += " " + CypherQueryHelper.getReturnPart("d");
    var result = new ArrayList<DataObject>();
    for (var obj : findByQuery(query, paramsMap)) {
      List<DataObject> parentList = obj.getParent() != null ? List.of(obj.getParent()) : Collections.emptyList();
      if (
        matchCollection(obj, collectionId) &&
        matchName(obj, params.getName()) &&
        matchRelated(parentList, params.getParentId()) &&
        matchRelated(obj.getSuccessors(), params.getSuccessorId()) &&
        matchRelated(obj.getPredecessors(), params.getPredecessorId())
      ) {
        result.add(obj);
      }
    }

    return result;
  }

  public List<DataObject> findByCollectionByShepardIds(
    long collectionShepardId,
    QueryParamHelper paramsWithShepardIds
  ) {
    return findByCollectionByShepardIds(collectionShepardId, paramsWithShepardIds, null);
  }

  /**
   * Searches the database for DataObjects.
   *
   * @param collectionShepardId  identifies the Collection
   * @param paramsWithShepardIds encapsulates possible parameters
   * @return a List of DataObjects
   */
  public List<DataObject> findByCollectionByShepardIds(
    long collectionShepardId,
    QueryParamHelper paramsWithShepardIds,
    UUID versionUID
  ) {
    Map<String, Object> paramsMap = new HashMap<>();
    paramsMap.put("name", paramsWithShepardIds.getName());
    if (paramsWithShepardIds.hasPagination()) {
      paramsMap.put("offset", paramsWithShepardIds.getPagination().getOffset());
      paramsMap.put("size", paramsWithShepardIds.getPagination().getSize());
    }
    String match =
      "MATCH (v:Version)<-[:has_version]-(c:Collection)-[hdo:has_dataobject]->" +
      CypherQueryHelper.getObjectPart("d", "DataObject", paramsWithShepardIds.hasName());
    String where = " WHERE c." + Constants.SHEPARD_ID + "=" + collectionShepardId + " AND ";
    //search in HEAD version
    if (versionUID == null) where = where + CypherQueryHelper.getVersionHeadPart("v");
    //search in version given by versionUID
    else where = where + CypherQueryHelper.getVersionPart("v", versionUID);
    if (paramsWithShepardIds.hasParentId()) {
      if (paramsWithShepardIds.getParentId() == -1) {
        where += " AND NOT EXISTS((d)<-[:has_child]-(:DataObject {deleted: FALSE}))";
      } else {
        match +=
          "<-[:has_child]-(parent:DataObject {deleted: FALSE, " +
          Constants.SHEPARD_ID +
          ": " +
          paramsWithShepardIds.getParentId() +
          "})";
      }
    }

    if (paramsWithShepardIds.hasPredecessorId()) {
      if (paramsWithShepardIds.getPredecessorId() == -1) {
        where += " AND NOT EXISTS((d)<-[:has_successor]-(:DataObject {deleted: FALSE}))";
      } else {
        match +=
          "<-[:has_successor]-(predecessor:DataObject {deleted: FALSE, " +
          Constants.SHEPARD_ID +
          ": " +
          paramsWithShepardIds.getPredecessorId() +
          "})";
      }
    }
    if (paramsWithShepardIds.hasSuccessorId()) {
      if (paramsWithShepardIds.getSuccessorId() == -1) {
        where += " AND NOT EXISTS((d)-[:has_successor]->(:DataObject {deleted: FALSE}))";
      } else {
        match +=
          "-[:has_successor]->(successor:DataObject {deleted: FALSE, " +
          Constants.SHEPARD_ID +
          ": " +
          paramsWithShepardIds.getSuccessorId() +
          "})";
      }
    }

    String query = match + where + " WITH d";
    if (paramsWithShepardIds.hasOrderByAttribute()) {
      query +=
        " " +
        CypherQueryHelper.getOrderByPart(
          "d",
          paramsWithShepardIds.getOrderByAttribute(),
          paramsWithShepardIds.getOrderDesc()
        );
    }
    if (paramsWithShepardIds.hasPagination()) {
      query += " " + CypherQueryHelper.getPaginationPart();
    }
    query += " " + CypherQueryHelper.getReturnPart("d");
    var result = new ArrayList<DataObject>();
    for (var obj : findByQuery(query, paramsMap)) {
      List<DataObject> parentList = obj.getParent() != null ? List.of(obj.getParent()) : Collections.emptyList();
      if (
        matchCollectionByShepardId(obj, collectionShepardId) &&
        matchName(obj, paramsWithShepardIds.getName()) &&
        matchRelatedByShepardId(parentList, paramsWithShepardIds.getParentId()) &&
        matchRelatedByShepardId(obj.getSuccessors(), paramsWithShepardIds.getSuccessorId()) &&
        matchRelatedByShepardId(obj.getPredecessors(), paramsWithShepardIds.getPredecessorId())
      ) {
        result.add(obj);
      }
    }

    return result;
  }

  /**
   * Delete dataObject and all related references
   *
   * @param id        identifies the dataObject
   * @param updatedBy current date
   * @param updatedAt current user
   * @return whether the deletion was successful or not
   */
  public boolean deleteDataObjectByNeo4jId(long id, User updatedBy, Date updatedAt) {
    var dataObject = findByNeo4jId(id);
    dataObject.setUpdatedBy(updatedBy);
    dataObject.setUpdatedAt(updatedAt);
    dataObject.setDeleted(true);
    createOrUpdate(dataObject);
    String query = String.format(
      "MATCH (d:DataObject) WHERE ID(d) = %d OPTIONAL MATCH (d)-[:has_reference]->(r:BasicReference) " +
      "FOREACH (n in [d,r] | SET n.deleted = true)",
      id
    );
    var result = runQuery(query, Collections.emptyMap());
    return result;
  }

  /**
   * Delete dataObject and all related references
   *
   * @param shepardId identifies the dataObject
   * @param updatedBy current date
   * @param updatedAt current user
   * @return whether the deletion was successful or not
   */
  public boolean deleteDataObjectByShepardId(long shepardId, User updatedBy, Date updatedAt) {
    DataObject dataObject = findByShepardId(shepardId);
    dataObject.setUpdatedBy(updatedBy);
    dataObject.setUpdatedAt(updatedAt);
    dataObject.setDeleted(true);
    createOrUpdate(dataObject);
    String query = String.format(
      "MATCH (d:DataObject) WHERE ID(d) = %d OPTIONAL MATCH (d)-[:has_reference]->(r:BasicReference) " +
      "FOREACH (n in [d,r] | SET n.deleted = true)",
      dataObject.getId()
    );
    var result = runQuery(query, Collections.emptyMap());
    return result;
  }

  private boolean matchName(DataObject obj, String name) {
    return name == null || name.equalsIgnoreCase(obj.getName());
  }

  private boolean matchRelated(List<DataObject> related, Long id) {
    if (id == null) {
      return true;
    } else if (id == -1) {
      // return true if there is no related object or all objects are deleted
      return related.stream().allMatch(DataObject::isDeleted);
    } else {
      // return true if at least one related object that is not deleted matches the ID
      return related.stream().anyMatch(d -> !d.isDeleted() && d.getId().equals(id));
    }
  }

  private boolean matchRelatedByShepardId(List<DataObject> related, Long shepardId) {
    if (shepardId == null) {
      return true;
    } else if (shepardId == -1) {
      // return true if there is no related object or all objects are deleted
      return related.stream().allMatch(DataObject::isDeleted);
    } else {
      // return true if at least one related object that is not deleted matches the ID
      return related.stream().anyMatch(d -> !d.isDeleted() && d.getShepardId().equals(shepardId));
    }
  }

  private boolean matchCollection(DataObject obj, long collectionId) {
    return obj.getCollection() != null && obj.getCollection().getId().equals(collectionId);
  }

  private boolean matchCollectionByShepardId(DataObject obj, long collectionShepardId) {
    return obj.getCollection() != null && obj.getCollection().getShepardId().equals(collectionShepardId);
  }

  public List<DataObject> getDataObjectsByQuery(String query) {
    var queryResult = findByQuery(query, Collections.emptyMap());
    List<DataObject> ret = StreamSupport.stream(queryResult.spliterator(), false).toList();
    return ret;
  }
}