LifeCycleDatabaseServiceImpl.java

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community 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://opensource.org/licenses/ecl2.txt
 *
 * 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.opencastproject.lifecyclemanagement.impl.persistence;

import static org.opencastproject.db.Queries.namedQuery;

import org.opencastproject.db.DBSession;
import org.opencastproject.db.DBSessionFactory;
import org.opencastproject.lifecyclemanagement.api.LifeCycleDatabaseException;
import org.opencastproject.lifecyclemanagement.api.LifeCycleDatabaseService;
import org.opencastproject.lifecyclemanagement.api.LifeCyclePolicy;
import org.opencastproject.lifecyclemanagement.api.LifeCycleTask;
import org.opencastproject.lifecyclemanagement.api.Status;
import org.opencastproject.lifecyclemanagement.impl.LifeCyclePolicyImpl;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.requests.SortCriterion;

import org.apache.commons.lang3.tuple.Pair;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

@Component(
    immediate = true,
    service = { LifeCycleDatabaseService.class },
    property = {
      "service.description=LifeCycle Database Service"
    })
public class LifeCycleDatabaseServiceImpl implements LifeCycleDatabaseService {
  /**
   * JPA persistence unit name
   */
  public static final String PERSISTENCE_UNIT = "org.opencastproject.lifecyclemanagement.impl.persistence";
  private static final Logger logger = LoggerFactory.getLogger(LifeCycleDatabaseServiceImpl.class);
  /**
   * Factory used to create {@link EntityManager}s for transactions
   */
  private EntityManagerFactory emf;

  private DBSessionFactory dbSessionFactory;
  private DBSession db;

  /**
   * The security service
   */
  protected SecurityService securityService;

  /**
   * OSGi DI
   */
  @Reference(target = "(osgi.unit.name=org.opencastproject.lifecyclemanagement.impl.persistence)")
  public void setEntityManagerFactory(EntityManagerFactory emf) {
    this.emf = emf;
  }

  @Reference
  public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
    this.dbSessionFactory = dbSessionFactory;
  }

  /**
   * OSGi callback to set the security service.
   *
   * @param securityService the securityService to set
   */
  @Reference(name = "security-service")
  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  @Activate
  public void activate(ComponentContext cc) {
    logger.info("Activating persistence manager for lifecycle management");
    db = dbSessionFactory.createSession(emf);
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#getLifeCyclePolicy(String)
   */
  @Override
  public LifeCyclePolicy getLifeCyclePolicy(String id) throws NotFoundException, LifeCycleDatabaseException {
    return getLifeCyclePolicy(id, securityService.getOrganization().getId());
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#getLifeCyclePolicy(String, String)
   */
  @Override
  public LifeCyclePolicy getLifeCyclePolicy(String id, String orgId)
          throws NotFoundException, LifeCycleDatabaseException {
    try {
      return db.execTxChecked(em -> {
        Optional<LifeCyclePolicy> lifeCyclePolicy = getLifeCyclePolicyById(id, orgId).apply(em);
        // TODO: Should we just return the optional instead?
        if (lifeCyclePolicy.isEmpty()) {
          throw new NotFoundException("No lifecycle policy with id=" + id + " exists");
        }
        return lifeCyclePolicy.get();
      });
    } catch (NotFoundException e) {
      throw e;
    } catch (Exception e) {
      logger.error("Could not retrieve lifecycle policy with ID '{}'", id, e);
      throw new LifeCycleDatabaseException(e);
    }
  }

  /**
   * {@inheritDoc}
   * @see LifeCycleDatabaseService#getLifeCyclePolicies(int, int, SortCriterion)
   */
  @Override
  public long getLifeCyclePoliciesTotal(String orgId) throws LifeCycleDatabaseException {
    try {
      return db.execTxChecked(em -> {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        final CriteriaQuery<Long> query = cb.createQuery(Long.class);
        Root<LifeCyclePolicyImpl> group = query.from(LifeCyclePolicyImpl.class);
        query.select(cb.count(group));

        TypedQuery<Long> typedQuery = em.createQuery(query);
        return typedQuery.getSingleResult();
      });
    } catch (Exception e) {
      logger.error("Could not find active lifecycle policies", e);
      throw new LifeCycleDatabaseException(e);
    }
  }

  /**
   * {@inheritDoc}
   * @see LifeCycleDatabaseService#getLifeCyclePolicies(int, int, SortCriterion)
   */
  @Override
  public List<LifeCyclePolicy> getLifeCyclePolicies(int limit, int offset, SortCriterion sortCriterion)
          throws LifeCycleDatabaseException {

    try {
      return db.exec(em -> {
        CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
        CriteriaQuery<LifeCyclePolicyImpl> criteriaQuery = criteriaBuilder.createQuery(LifeCyclePolicyImpl.class);
        Root<LifeCyclePolicyImpl> from = criteriaQuery.from(LifeCyclePolicyImpl.class);
        CriteriaQuery<LifeCyclePolicyImpl> select = criteriaQuery.select(from);

        if (sortCriterion.getOrder().equals(SortCriterion.Order.Ascending)) {
          criteriaQuery.orderBy(criteriaBuilder.asc(from.get(sortCriterion.getFieldName())));
        } else if (sortCriterion.getOrder().equals(SortCriterion.Order.Descending)) {
          criteriaQuery.orderBy(criteriaBuilder.desc(from.get(sortCriterion.getFieldName())));
        }

        TypedQuery<LifeCyclePolicyImpl> allQuery = em.createQuery(select);

        allQuery.setMaxResults(limit);
        allQuery.setFirstResult(offset);

        return (List<LifeCyclePolicy>)(List<?>)allQuery.getResultList();
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Error fetching policies from database", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#getActiveLifeCyclePolicies(String)
   */
  @Override
  public List<LifeCyclePolicy> getActiveLifeCyclePolicies(String orgId)
          throws LifeCycleDatabaseException {
    try {
      return db.execTxChecked(em -> {
        var lifeCyclePolicies = namedQuery.findAll(
            "LifeCyclePolicy.findActive",
            LifeCyclePolicy.class,
            Pair.of("organizationId", orgId)
        ).apply(em);
        return lifeCyclePolicies;
      });
    } catch (Exception e) {
      logger.error("Could not find active lifecycle policies", e);
      throw new LifeCycleDatabaseException(e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#createLifeCyclePolicy(LifeCyclePolicy, String)
   */
  @Override
  public LifeCyclePolicy createLifeCyclePolicy(LifeCyclePolicy policy, String orgId) throws LifeCycleDatabaseException {
    try {
      return db.execTx(em -> {
        policy.setOrganization(orgId);
        em.persist(policy);
        return policy;
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Could not create policy with ID '" + policy.getId() + "'", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#updateLifeCyclePolicy(LifeCyclePolicy, String)
   */
  @Override
  public boolean updateLifeCyclePolicy(LifeCyclePolicy policy, String orgId) throws LifeCycleDatabaseException {
    try {
      return db.execTx(em -> {
        Optional<LifeCyclePolicy> fromDb = getLifeCyclePolicyById(policy.getId(), orgId).apply(em);
        if (fromDb.isEmpty()) {
          return false;
        } else {
          em.merge(policy);
          return true;
        }
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Could not update policy with ID '" + policy.getId() + "'", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#deleteLifeCyclePolicy(LifeCyclePolicy, String)
   */
  @Override
  public boolean deleteLifeCyclePolicy(LifeCyclePolicy policy, String orgId) throws LifeCycleDatabaseException {
    try {
      return db.execTx(em -> {
        Optional<LifeCyclePolicy> fromDb = getLifeCyclePolicyById(policy.getId(), orgId).apply(em);
        if (fromDb.isPresent()) {
          em.remove(fromDb.get());
          logger.debug("LifeCyclePolicy with id {} was deleted.", fromDb.get().getId());

          return true;
        }
        return false;
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Could not delete policy with ID '" + policy.getId() + "'", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#deleteLifeCyclePolicy(LifeCyclePolicy, String)
   */
  @Override
  public void deleteAllLifeCyclePoliciesCreatedByConfig(String orgId) throws LifeCycleDatabaseException {
    try {
      db.execTx(em -> {
        namedQuery.delete(
          "LifeCyclePolicy.deleteAllCreatedFromConfig",
          Pair.of("organizationId", orgId)
        ).apply(em);
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Could not delete policy created from config. ", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#getLifeCycleTask(String)
   */
  @Override
  public LifeCycleTask getLifeCycleTask(String id) throws NotFoundException, LifeCycleDatabaseException {
    try {
      return db.execTxChecked(em -> {
        String orgId = securityService.getOrganization().getId();
        Optional<LifeCycleTask> lifeCycleTask = getLifeCycleTaskById(id, orgId).apply(em);
        if (lifeCycleTask.isEmpty()) {
          throw new NotFoundException("No lifecycle task with id=" + id + " exists");
        }
        return lifeCycleTask.get();
      });
    } catch (NotFoundException e) {
      throw e;
    } catch (Exception e) {
      logger.error("Could not retrieve lifecycle task with ID '{}'", id, e);
      throw new LifeCycleDatabaseException(e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#getLifeCycleTaskByTargetId(String)
   */
  @Override
  public LifeCycleTask getLifeCycleTaskByTargetId(String targetId)
          throws NotFoundException, LifeCycleDatabaseException {
    try {
      return db.execTxChecked(em -> {
        String orgId = securityService.getOrganization().getId();
        Optional<LifeCycleTask> lifeCycleTask = namedQuery
            .findOpt(
                "LifeCycleTask.findByTargetId",
                LifeCycleTask.class,
                Pair.of("targetId", targetId),
                Pair.of("organizationId", orgId)
            ).apply(em);
        if (lifeCycleTask.isEmpty()) {
          throw new NotFoundException("No lifecycle task with targetId=" + targetId + " exists");
        }
        return lifeCycleTask.get();
      });
    } catch (NotFoundException e) {
      throw e;
    } catch (Exception e) {
      logger.error("Could not retrieve lifecycle task with targetId ID '{}'", targetId, e);
      throw new LifeCycleDatabaseException(e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#getLifeCycleTasksWithStatus(Status, String)
   */
  @Override
  public List<LifeCycleTask> getLifeCycleTasksWithStatus(Status status, String orgId)
          throws LifeCycleDatabaseException {
    try {
      return db.execTxChecked(em -> {
        var lifeCycleTasks = namedQuery.findAll(
            "LifeCycleTask.withStatus",
            LifeCycleTask.class,
            Pair.of("status", status),
            Pair.of("organizationId", orgId)
        ).apply(em);
        return lifeCycleTasks;
      });
    } catch (Exception e) {
      logger.error("Could not find lifecycle tasks with status: {}", status, e);
      throw new LifeCycleDatabaseException(e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#createLifeCycleTask(LifeCycleTask, String)
   */
  @Override
  public LifeCycleTask createLifeCycleTask(LifeCycleTask task, String orgId) throws LifeCycleDatabaseException {
    try {
      if (task.getId() != null) {
        throw new LifeCycleDatabaseException("Id must not be set when creating new task");
      }
      return db.execTx(em -> {
        task.setOrganization(orgId);
        task.setCreationDate(new Date());
        em.persist(task);
        return task;
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Could not create task with ID '" + task.getId() + "'", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#updateLifeCycleTask(LifeCycleTask, String)
   */
  @Override
  public boolean updateLifeCycleTask(LifeCycleTask task, String orgId) throws LifeCycleDatabaseException {
    try {
      return db.execTx(em -> {
        Optional<LifeCycleTask> fromDb = getLifeCycleTaskById(task.getId(), orgId).apply(em);
        if (fromDb.isEmpty()) {
          return false;
        } else {
          em.merge(task);
          return true;
        }
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Could not update task with ID '" + task.getId() + "'", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see LifeCycleDatabaseService#deleteLifeCycleTask(LifeCycleTask, String)
   */
  @Override
  public boolean deleteLifeCycleTask(LifeCycleTask task, String orgId) throws LifeCycleDatabaseException {
    try {
      return db.execTx(em -> {
        Optional<LifeCycleTask> fromDb = getLifeCycleTaskById(task.getId(), orgId).apply(em);
        if (fromDb.isPresent()) {
          em.remove(fromDb.get());
          logger.debug("LifeCycleTask with id {} was deleted.", fromDb.get().getId());
          return true;
        }
        return false;
      });
    } catch (Exception e) {
      throw new LifeCycleDatabaseException("Could not delete task with ID '" + task.getId() + "'", e);
    }
  }

  /**
   * Gets a life cycle policy by its ID, using the current organizational context.
   *
   * @param id    the policy identifier
   * @param orgId the organisation identifier
   * @return the policy in an optional
   */
  protected Function<EntityManager, Optional<LifeCyclePolicy>> getLifeCyclePolicyById(String id, String orgId) {
    return namedQuery.findOpt("LifeCyclePolicy.findById", LifeCyclePolicy.class, Pair.of("id", id),
        Pair.of("organizationId", orgId));
  }

  /**
   * Gets a life cycle task by its ID, using the current organizational context.
   *
   * @param id    the task identifier
   * @param orgId the organisation identifier
   * @return the task in an optional
   */
  protected Function<EntityManager, Optional<LifeCycleTask>> getLifeCycleTaskById(String id, String orgId) {
    return namedQuery.findOpt("LifeCycleTask.findById", LifeCycleTask.class, Pair.of("id", id),
        Pair.of("organizationId", orgId));
  }
}