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

ca.gc.aafc.dina.service.AuditService Maven / Gradle / Ivy

package ca.gc.aafc.dina.service;

import ca.gc.aafc.dina.security.DinaAuthenticatedUser;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.javers.core.Javers;
import org.javers.core.commit.CommitId;
import org.javers.core.metamodel.object.CdoSnapshot;
import org.javers.repository.jql.QueryBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@ConditionalOnProperty(value = "dina.auditing.enabled", havingValue = "true")
public class AuditService {

  private final Javers javers;
  private final NamedParameterJdbcTemplate jdbcTemplate;
  private final Optional user;

  public static final String ANONYMOUS = "anonymous";

  /**
   * Returns true if the given audit instance has a terminal snapshot associated with it. Terminal snapshots
   * represent delete operations.
   *
   * @param auditInstance The audit instance to check
   * @return true if the given audit instance has a terminal snapshot associated with it
   */
  public boolean hasTerminalSnapshot(@NonNull AuditInstance auditInstance) {
    return this.findAll(auditInstance, null, Integer.MAX_VALUE, 0)
      .stream()
      .anyMatch(CdoSnapshot::isTerminal);
  }

  /**
   * Returns a list of Audit snapshots filtered by a given instance and author.
   * Author and instance can be null for un-filtered results.
   *
   * @param instance - instance to filter may be null
   * @param author   - author to filter may be null
   * @param limit    - limit of results
   * @param skip     - amount of results to skip
   * @return list of Audit snapshots
   */
  public List findAll(AuditInstance instance, String author, int limit, int skip) {
    return AuditService.findAll(this.javers, instance, author, limit, skip);
  }

  /**
   * Get the total resource count by a given Author and Audit Instance. Author and
   * instance can be null for un-filtered counts.
   *
   * @param author   - author to filter
   * @param instance - instance to filter
   * @return the total resource count
   */
  public Long getResouceCount(String author, AuditInstance instance) {
    return AuditService.getResouceCount(this.jdbcTemplate, author, instance);
  }

  /**
   * Removes the snapshots for a given instance.
   *
   * @param instance - instance to remove
   */
  @Transactional
  public void removeSnapshots(@NonNull AuditInstance instance) {
    List snapshots = javers.findSnapshots(
      QueryBuilder.byInstanceId(instance.getId(), instance.getType()).build());
    for (CdoSnapshot snap : snapshots) {
      CommitId commitId = snap.getCommitId();
      MapSqlParameterSource idMap = new MapSqlParameterSource(Map.of(
        "id",
        commitId.valueAsNumber()));

      String snapShotDelete = "delete from jv_snapshot where commit_fk = (select commit_pk from jv_commit where commit_id = :id)";
      jdbcTemplate.update(snapShotDelete, idMap);

      String commitDelete = "delete from jv_commit where commit_id = :id";
      jdbcTemplate.update(commitDelete, idMap);

      String commitPropertiesDelete = "delete from jv_commit_property where commit_fk = (select commit_pk from jv_commit where commit_id = :id)";
      jdbcTemplate.update(commitPropertiesDelete, idMap);
    }

    String globalDelete = "delete from jv_global_id where local_id = :id and type_name = :type";
    jdbcTemplate.update(globalDelete, new MapSqlParameterSource(
      Map.of("id", instance.getId(), "type", instance.getType())));
  }

  /**
   * Commit a snapshot for a given object, A dina authenticated user will be set
   * as the commit author. If a user is not present the author will be anonymous.
   *
   * @param obj - domain object state to persist
   */
  public void audit(@NonNull Object obj) {
    if (user.isPresent()) {
      this.javers.commit(user.get().getUsername(), obj);
    } else {
      this.javers.commit(ANONYMOUS, obj);
    }
  }

  /**
   * Commit a shallow delete snapshot for a given object, A dina authenticated
   * user will be set as the commit author. If a user is not present the author
   * will be anonymous.
   *
   * @param obj - domain object state to persist
   */
  public void auditDeleteEvent(@NonNull Object obj) {
    if (user.isPresent()) {
      this.javers.commitShallowDelete(user.get().getUsername(), obj);
    } else {
      this.javers.commitShallowDelete(ANONYMOUS, obj);
    }
  }

  /**
   * Returns a list of Audit snapshots using a given Javers Facade filtered by a
   * given instance and author. Author and instance can be null for un-filtered
   * results.
   *
   * @param javers   - Facade to query
   * @param instance - instance to filter may be null
   * @param author   - author to filter may be null
   * @param limit    - limit of results
   * @param skip     - amount of results to skip
   * @return list of Audit snapshots
   */
  public static List findAll(Javers javers, AuditInstance instance, String author, int limit, int skip) {
    QueryBuilder queryBuilder;

    if (instance != null) {
      queryBuilder = QueryBuilder.byInstanceId(instance.getId(), instance.getType());
    } else {
      queryBuilder = QueryBuilder.anyDomainObject();
    }

    if (StringUtils.isNotBlank(author)) {
      queryBuilder.byAuthor(author);
    }

    queryBuilder.limit(limit);
    queryBuilder.skip(skip);

    return javers.findSnapshots(queryBuilder.build());
  }

  /**
   * Get the total resource count by a given Author and/or Audit Instance. Author
   * and instance can be null for un-filtered counts.
   *
   * @param jdbc     - NamedParameterJdbcTemplate for the query
   * @param author   - author to filter
   * @param instance - instance filter to apply
   * @return the total resource count
   */
  public static Long getResouceCount(@NonNull NamedParameterJdbcTemplate jdbc, String author, AuditInstance instance) {

    String id = null;
    String type = null;

    if (instance != null) {
      id = instance.getId();
      type = instance.getType();
    }

    SqlParameterSource parameters = new MapSqlParameterSource()
      .addValue("author", author)
      .addValue("id", id)
      .addValue("type", type);

    String sql = getResouceCountSql(author, id);
    return jdbc.queryForObject(sql, parameters, Long.class);
  }

  /**
   * Returns the needed SQL String to return a resouce count for a specific author
   * and id. Author and id can be null for un-filtered counts.
   *
   * @param author - author filter to apply
   * @param id     - id filter to apply
   * @return SQL String to return a resouce count
   */
  private static String getResouceCountSql(String author, String id) {
    String baseSql = "select count(*) from jv_snapshot s join jv_commit c on s.commit_fk = c.commit_pk where 1=1 %s %s ;";
    return String.format(
      baseSql,
      StringUtils.isNotBlank(author) ? "and c.author = :author" : "",
      StringUtils.isNotBlank(id)
        ? "and global_id_fk = (select global_id_pk from jv_global_id where local_id = :id and type_name = :type)"
        : "");
  }

  @Builder
  @Data
  public static final class AuditInstance {

    @NonNull
    private final String type;
    @NonNull
    private final String id;

    /**
     * Returns an Optional AuditInstance from a string representation or empty if the
     * string is blank. Expected string format is {type}/{id}.
     *
     * @param instanceString - string to parse
     * @throws IllegalArgumentException if the string has an invalid format.
     * @return Optional AuditInstance or empty for blank strings
     */
    public static Optional fromString(String instanceString) {
      if (StringUtils.isBlank(instanceString)) {
        return Optional.empty();
      }

      String[] split = instanceString.split("/");
      if (split.length != 2) {
        throw new IllegalArgumentException(
          "Invalid ID must be formatted as {type}/{id}: " + instanceString);
      }
      return Optional.of(AuditInstance.builder().type(split[0]).id(split[1]).build());
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy