DataObjectService.java

package de.dlr.shepard.context.collection.services;

import de.dlr.shepard.auth.permission.services.PermissionsService;
import de.dlr.shepard.auth.security.AuthenticationContext;
import de.dlr.shepard.auth.users.entities.User;
import de.dlr.shepard.auth.users.services.UserService;
import de.dlr.shepard.common.exceptions.InvalidAuthException;
import de.dlr.shepard.common.exceptions.InvalidBodyException;
import de.dlr.shepard.common.exceptions.InvalidPathException;
import de.dlr.shepard.common.exceptions.InvalidRequestException;
import de.dlr.shepard.common.util.DateHelper;
import de.dlr.shepard.common.util.QueryParamHelper;
import de.dlr.shepard.context.collection.daos.DataObjectDAO;
import de.dlr.shepard.context.collection.entities.Collection;
import de.dlr.shepard.context.collection.entities.DataObject;
import de.dlr.shepard.context.collection.io.DataObjectIO;
import de.dlr.shepard.context.references.dataobject.daos.DataObjectReferenceDAO;
import de.dlr.shepard.context.references.dataobject.entities.DataObjectReference;
import de.dlr.shepard.context.version.services.VersionService;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;

@RequestScoped
public class DataObjectService {

  @Inject
  DataObjectDAO dataObjectDAO;

  @Inject
  DataObjectReferenceDAO dataObjectReferenceDAO;

  @Inject
  UserService userService;

  @Inject
  DateHelper dateHelper;

  @Inject
  VersionService versionService;

  @Inject
  CollectionService collectionService;

  @Inject
  PermissionsService permissionsService;

  @Inject
  AuthenticationContext authenticationContext;

  /**
   * Creates a DataObject
   *
   * @param collectionShepardId identifies the Collection
   * @param dataObject          to be stored
   * @return the stored DataObject with the auto generated id
   * @throws InvalidPathException if collection with collectionShepardId does not
   *                              exist
   */
  public DataObject createDataObject(long collectionShepardId, DataObjectIO dataObject) {
    Collection collection = collectionService.getCollection(collectionShepardId);
    collectionService.assertIsAllowedToEditCollection(collectionShepardId);

    User user = userService.getCurrentUser();
    DataObject parent = findRelatedDataObject(collection.getShepardId(), dataObject.getParentId(), null);
    List<DataObject> predecessors = findRelatedDataObjects(
      collection.getShepardId(),
      dataObject.getPredecessorIds(),
      null
    );
    DataObject toCreate = new DataObject();
    toCreate.setAttributes(dataObject.getAttributes());
    toCreate.setDescription(dataObject.getDescription());
    toCreate.setName(dataObject.getName());
    toCreate.setCollection(collection);
    toCreate.setParent(parent);
    toCreate.setPredecessors(predecessors);
    toCreate.setCreatedAt(dateHelper.getDate());
    toCreate.setCreatedBy(user);
    DataObject created = dataObjectDAO.createOrUpdate(toCreate);
    created.setShepardId(created.getId());
    created = dataObjectDAO.createOrUpdate(created);
    versionService.attachToVersionOfVersionableEntityAndReturnVersion(collectionShepardId, created.getShepardId());
    return created;
  }

  /**
   * Get DataObject
   *
   * @param shepardId identifies the searched dataObject
   * @return the DataObject with the given id
   * @throws InvalidPathException if the DataObject cannot be found
   * @throws InvalidAuthException if user does not have read permissions on the
   *                              data object's collection
   */
  public DataObject getDataObject(long shepardId) {
    return getDataObject(shepardId, null);
  }

  /**
   * Get DataObject
   *
   * @param shepardId  identifies the searched dataObject
   * @param versionUID the dataobject's version UUID
   * @return an Optional containing the DataObject with the given id
   * @throws InvalidPathException if DataObject (with version UUID) cannot be
   *                              found
   * @throws InvalidAuthException if user does not have read permissions on the
   *                              data object's collection
   */
  public DataObject getDataObject(long shepardId, UUID versionUID) {
    DataObject ret;
    String errorMsg;
    if (versionUID == null) {
      ret = dataObjectDAO.findByShepardId(shepardId);
      errorMsg = String.format("DataObject with id %s is null or deleted", shepardId);
    } else {
      ret = dataObjectDAO.findByShepardId(shepardId, versionUID);
      errorMsg = String.format("DataObject with id %s and versionUID %s is null or deleted", shepardId, versionUID);
    }
    if (ret == null || ret.isDeleted()) {
      Log.error(errorMsg);
      throw new InvalidPathException("ID ERROR - " + errorMsg);
    }

    collectionService.assertIsAllowedToReadCollection(ret.getCollection().getShepardId());
    cutDeleted(ret);

    HashSet<Long> incomingReferencesIdList = new HashSet<Long>();
    for (DataObjectReference reference : ret.getIncoming()) incomingReferencesIdList.add(reference.getId());
    List<DataObjectReference> completeIncomingReferences = new ArrayList<DataObjectReference>();
    for (Long id : incomingReferencesIdList) completeIncomingReferences.add(dataObjectReferenceDAO.findByNeo4jId(id));

    HashSet<Long> childrenIdList = new HashSet<Long>();
    for (DataObject child : ret.getChildren()) childrenIdList.add(child.getId());
    List<DataObject> completeChildren = new ArrayList<DataObject>();
    for (Long id : childrenIdList) completeChildren.add(dataObjectDAO.findByNeo4jId(id));

    HashSet<Long> predecessorsIdList = new HashSet<Long>();
    for (DataObject predecessor : ret.getPredecessors()) predecessorsIdList.add(predecessor.getId());
    List<DataObject> completePredecessors = new ArrayList<DataObject>();
    for (Long id : predecessorsIdList) completePredecessors.add(dataObjectDAO.findByNeo4jId(id));

    HashSet<Long> successorsIdList = new HashSet<Long>();
    for (DataObject successor : ret.getSuccessors()) successorsIdList.add(successor.getId());
    List<DataObject> completeSuccessors = new ArrayList<DataObject>();
    for (Long id : successorsIdList) completeSuccessors.add(dataObjectDAO.findByNeo4jId(id));

    ret.setChildren(completeChildren);
    ret.setIncoming(completeIncomingReferences);
    ret.setPredecessors(completePredecessors);
    ret.setSuccessors(completeSuccessors);
    if (ret.getParent() != null) ret.setParent(dataObjectDAO.findByNeo4jId(ret.getParent().getId()));
    return ret;
  }

  /**
   * Get DataObject
   *
   * @param collectionShepardId collection's shepardId
   * @param shepardId           identifies the searched dataObject
   * @return the DataObject with the given id
   * @throws InvalidPathException if dataobject or collection cannot be found or
   *                              the dataobject does not match the collection
   * @throws InvalidAuthException if user does not have read permissions on the
   *                              collection
   */
  public DataObject getDataObject(long collectionShepardId, long shepardId) {
    return getDataObject(collectionShepardId, shepardId, null);
  }

  /**
   * Get DataObject
   *
   * @param collectionShepardId collection's shepardId
   * @param shepardId           identifies the searched dataObject
   * @param versionUID          the DataObject's version UUID
   * @return the DataObject with the given id
   * @throws InvalidPathException if DataObject or collection cannot be found or
   *                              the DataObject does not match the collection
   * @throws InvalidAuthException if user does not have read permissions on the
   *                              collection
   */
  public DataObject getDataObject(long shepardCollectionId, long shepardId, UUID versionUID) {
    collectionService.getCollection(shepardCollectionId);

    DataObject dataObject;
    try {
      // This may throw a 403 if the data object is in a different collection for
      // which the user does not have permissions -> handle that exception
      // specifically
      dataObject = getDataObject(shepardId, versionUID);
    } catch (InvalidAuthException ex) {
      throw new InvalidPathException("ID ERROR - There is no association between collection and dataObject");
    }

    if (!dataObject.getCollection().getShepardId().equals(shepardCollectionId)) {
      throw new InvalidPathException("ID ERROR - There is no association between collection and dataObject");
    }
    return dataObject;
  }

  /**
   * Searches the database for DataObjects.
   *
   * @param collectionShepardId  identifies the collection
   * @param paramsWithShepardIds encapsulates possible parameters
   * @param versionUID           identifies the version
   * @return a List of DataObjects
   * @throws InvalidPathException if collection with collectionShepardId does not
   *                              exist
   * @throws InvalidAuthException if user does not have read permissions on the
   *                              collection
   */
  public List<DataObject> getAllDataObjectsByShepardIds(
    long collectionShepardId,
    QueryParamHelper paramsWithShepardIds,
    UUID versionUID
  ) {
    collectionService.getCollection(collectionShepardId, versionUID);

    var unfiltered = dataObjectDAO.findByCollectionByShepardIds(collectionShepardId, paramsWithShepardIds, versionUID);
    var dataObjects = unfiltered.stream().map(this::cutDeleted).toList();
    return dataObjects;
  }

  /**
   * Updates a DataObject with new attributes. Hereby only not null attributes
   * will replace the old attributes.
   *
   * @param collectionShepardId ShepardId of the collection the dataobject is
   *                            assigned to
   * @param dataObjectShepardId Identifies the dataObject
   * @param dataObject          DataObject entity for updating.
   * @return updated DataObject.
   * @throws InvalidPathException if dataobject cannot be found or collection with
   *                              collectionShepardId does not exist
   * @throws InvalidAuthException if user does not have read or write permissions
   *                              on the collection
   */
  public DataObject updateDataObject(long collectionShepardId, long dataObjectShepardId, DataObjectIO dataObject) {
    DataObject old = getDataObject(collectionShepardId, dataObjectShepardId);
    collectionService.assertIsAllowedToEditCollection(collectionShepardId);

    User user = userService.getCurrentUser();

    if (old.getParent() != null) {
      dataObjectDAO.deleteHasChildRelation(old.getParent().getShepardId(), old.getShepardId());
    }
    if (old.getPredecessors() != null) {
      old
        .getPredecessors()
        .forEach(predecessor -> {
          dataObjectDAO.deleteHasSuccessorRelation(predecessor.getShepardId(), old.getShepardId());
        });
    }

    DataObject newParent = findRelatedDataObject(
      old.getCollection().getShepardId(),
      dataObject.getParentId(),
      dataObjectShepardId
    );
    List<DataObject> newPredecessors = findRelatedDataObjects(
      old.getCollection().getShepardId(),
      dataObject.getPredecessorIds(),
      dataObjectShepardId
    );

    old.setShepardId(old.getShepardId());
    old.setName(dataObject.getName());
    old.setDescription(dataObject.getDescription());
    old.setAttributes(dataObject.getAttributes());
    old.setParent(newParent);
    old.setPredecessors(newPredecessors);
    old.setUpdatedAt(dateHelper.getDate());
    old.setUpdatedBy(user);

    DataObject updated = dataObjectDAO.createOrUpdate(old);
    cutDeleted(updated);
    return updated;
  }

  /**
   * set the deleted flag for the DataObject
   *
   * @param collectionShepardId ShepardId of the collection the dataobject is
   *                            assigned to
   * @param dataObjectShepardId identifies the DataObject to be deleted
   * @return a boolean to identify if the DataObject was successfully removed
   * @throws InvalidPathException if dataobject cannot be found or collection with
   *                              collectionShepardId does not exist
   * @throws InvalidAuthException if user does not have read or write permissions
   *                              on the collection
   */
  public void deleteDataObject(long collectionShepardId, long dataObjectShepardId) {
    getDataObject(collectionShepardId, dataObjectShepardId);
    collectionService.assertIsAllowedToEditCollection(collectionShepardId);

    Date date = dateHelper.getDate();
    User user = userService.getCurrentUser();

    if (!dataObjectDAO.deleteDataObjectByShepardId(dataObjectShepardId, user, date)) {
      throw new InvalidRequestException(
        String.format("Could not delete DataObject with ShepardId %s", dataObjectShepardId)
      );
    }
  }

  private DataObject cutDeleted(DataObject dataObject) {
    var incoming = dataObject.getIncoming().stream().filter(i -> !i.isDeleted()).toList();
    dataObject.setIncoming(incoming);
    if (dataObject.getParent() != null && dataObject.getParent().isDeleted()) {
      dataObject.setParent(null);
    }
    var children = dataObject.getChildren().stream().filter(s -> !s.isDeleted()).toList();
    dataObject.setChildren(children);
    var predecessors = dataObject.getPredecessors().stream().filter(s -> !s.isDeleted()).toList();
    dataObject.setPredecessors(predecessors);
    var successors = dataObject.getSuccessors().stream().filter(s -> !s.isDeleted()).toList();
    dataObject.setSuccessors(successors);
    var references = dataObject.getReferences().stream().filter(ref -> !ref.isDeleted()).toList();
    dataObject.setReferences(references);
    return dataObject;
  }

  private List<DataObject> findRelatedDataObjects(
    long collectionShepardId,
    long[] referencedShepardIds,
    Long dataObjectShepardId
  ) {
    if (referencedShepardIds == null) return new ArrayList<>();

    var result = new ArrayList<DataObject>(referencedShepardIds.length);
    /*
     * TODO: seems to be inefficient since this loops generates referencedIds.length
     * calls to Neo4j this could possibly be packed into one query (or in chunks of
     * queries in case of a large referencedIds array)
     */
    for (var shepardId : referencedShepardIds) {
      result.add(findRelatedDataObject(collectionShepardId, shepardId, dataObjectShepardId));
    }
    return result;
  }

  private DataObject findRelatedDataObject(
    long collectionShepardId,
    Long referencedShepardId,
    Long dataObjectShepardId
  ) {
    if (referencedShepardId == null) return null;
    else if (referencedShepardId.equals(dataObjectShepardId)) throw new InvalidBodyException(
      "Self references are not allowed."
    );

    var dataObject = dataObjectDAO.findByShepardId(referencedShepardId);
    if (dataObject == null || dataObject.isDeleted()) throw new InvalidBodyException(
      String.format("The DataObject with id %d could not be found.", referencedShepardId)
    );

    // Prevent cross collection references
    if (!dataObject.getCollection().getShepardId().equals(collectionShepardId)) throw new InvalidBodyException(
      "Related data objects must belong to the same collection as the new data object"
    );

    return dataObject;
  }

  /**
   * Only needed for fixing session problems in unit tests
   */
  public void clearSession() {
    dataObjectDAO.clearSession();
  }
}