tripal_chado/src/Task/ChadoCloner.php

Summary

Maintainability
A
1 hr
Test Coverage
F
47%
<?php

namespace Drupal\tripal_chado\Task;

use Drupal\tripal_chado\Task\ChadoTaskBase;
use Drupal\tripal_biodb\Exception\TaskException;
use Drupal\tripal_biodb\Exception\LockException;
use Drupal\tripal_biodb\Exception\ParameterException;

/**
 * Chado cloner.
 *
 * Usage:
 * @code
 * // Where 'chado' is the name of an existing Chado schema and 'new_chado_copy'
 * // is the name of an unexisting schema that will be created and receive the
 * // full copy of the 'chado' schema content.
 * $cloner = \Drupal::service('tripal_chado.cloner');
 * $cloner->setParameters([
 *   'input_schemas'  => ['chado'],
 *   'output_schemas' => ['new_chado_copy'],
 * ]);
 * if (!$cloner->performTask()) {
 *   // Display a message telling the user the task failed and details are in
 *   // the site logs.
 * }
 * @endcode
 */
class ChadoCloner extends ChadoTaskBase {

  /**
   * Name of the task.
   */
  public const TASK_NAME = 'cloner';

  /**
   * Validate task parameters.
   *
   * Parameter array provided to the class constructor must include one input
   * schema and one output schema as shown:
   * ```
   * ['input_schemas' => ['original_name'], 'output_schemas' => ['copy_name'], ]
   * ```
   *
   * @throws \Drupal\tripal_biodb\Exception\ParameterException
   *   A descriptive exception is thrown in cas of invalid parameters.
   */
  public function validateParameters() :void {
    try {
      // Check input.
      if (empty($this->parameters['input_schemas'])
          || (1 != count($this->parameters['input_schemas']))
      ) {
        throw new ParameterException(
          "Invalid number of input schemas. Only one input schema must be specified."
        );
      }
      // Check output.
      if (empty($this->parameters['output_schemas'])
          || (1 != count($this->parameters['output_schemas']))
      ) {
        throw new ParameterException(
          "Invalid number of output schemas. Only one output schema must be specified."
        );
      }
      // Make sure both schemas are in the same database.
      $input_schema = $this->inputSchemas[0];
      $output_schema = $this->outputSchemas[0];
      if ($input_schema->getDatabaseName() != $output_schema->getDatabaseName()) {
        throw new ParameterException(
          "Schemas must reside in a same database. Could not clone schemas from one database to another with this implementation."
        );
      }
      $tripal_dbx = \Drupal::service('tripal.dbx');

      // Note: schema names have already been validated through BioConnection.
      // Check if the target schema is free.
      if ($output_schema->schema()->schemaExists()) {
        throw new ParameterException(
          'Target schema "'
          . $output_schema->getSchemaName()
          . '" already exists. Please remove that schema first.'
        );
      }

      // Check target name is not reserved.
      $issue = $tripal_dbx->isInvalidSchemaName($output_schema->getSchemaName());
      if ($issue) {
        throw new ParameterException($issue);
      }

      // Check if the source schema exists.
      if (!$input_schema->schema()->schemaExists()) {
        throw new ParameterException(
          'The source schema to clone "'
          . $input_schema->getSchemaName()
          . '" does not exist. Please select an existing schema to clone.'
        );
      }
    }
    catch (\Exception $e) {
      // Log.
      $this->logger->error($e->getMessage());
      // Rethrow.
      throw $e;
    }
  }

  /**
   * Clone a given chado schema into the specified schema.
   *
   * The cloning procedure uses a custom PostgreSQL function
   * (tripal_clone_schema() installed with Tripal Chado) to clone schema rather
   * than using a schema dump, modifying it to change schema name (with possible
   * side effects) and reloading that dump. It is faster and avoids using
   * temporary files (risks of content disclosure).
   *
   * Task parameter array provided to the class constructor includes:
   * - 'input_schemas' array: one input schema that must exist (required)
   * - 'output_schemas' array: one output schema that must not exist (required)
   *
   * Example:
   * ```
   * ['input_schemas' => ['original_name'], 'output_schemas' => ['copy_name'], ]
   * ```
   *
   * @return bool
   *   TRUE if the task was performed with success and FALSE if the task was
   *   completed but without the expected success.
   *
   * @throws Drupal\tripal_biodb\Exception\TaskException
   *   Thrown when a major failure prevents the task from being performed.
   *
   * @throws \Drupal\tripal_biodb\Exception\ParameterException
   *   Thrown if parameters are incorrect.
   *
   * @throws Drupal\tripal_biodb\Exception\LockException
   *   Thrown when the locks can't be acquired.
   */
  public function performTask() :bool {
    // Task return status.
    $task_success = FALSE;

    // Validate parameters.
    $this->validateParameters();

    // Acquire locks.
    $success = $this->acquireTaskLocks();
    if (!$success) {
      throw new LockException("Unable to acquire all locks for task. See logs for details.");
    }

    try
    {
      $source_schema = $this->inputSchemas[0];
      $target_schema = $this->outputSchemas[0];
      $tripal_dbx = \Drupal::service('tripal.dbx');

      // Get initial database size.
      $db_size = $tripal_dbx->getDatabaseSize($target_schema);

      // Get Chado size.
      $chado_size = $source_schema->schema()->getSchemaSize();

      // Save task initial data for later progress computation.
      $data = ['db_size' => $db_size, 'chado_size' => $chado_size];
      $this->state->set(static::STATE_KEY_DATA_PREFIX . $this->id, $data);

      // Clone schema.
      $drupal_schema = $tripal_dbx->getDrupalSchemaName();
      $args = [
        ':source' => $source_schema->getSchemaName(),
        ':target' => $target_schema->getSchemaName(),
      ];
      $sql_query =
        "SELECT $drupal_schema.tripal_clone_schema(:source, :target, TRUE, FALSE);"
      ;
      $this->connection->query($sql_query, $args);
      $this->logger->info("Schema cloning completed\n");

      // Check target schema exists.
      if ($target_schema->schema()->schemaExists()) {
        $task_success = TRUE;
      }

      // Release all locks.
      $this->releaseTaskLocks();

      // Cleanup state API.
      $this->state->delete(static::STATE_KEY_DATA_PREFIX . $this->id);
    }
    catch (\Exception $e) {
      $this->logger->error($e->getMessage());
      // Cleanup state API.
      $this->state->delete(static::STATE_KEY_DATA_PREFIX . $this->id);
      // Release all locks.
      $this->releaseTaskLocks();

      throw new TaskException(
        "Failed to complete schema cloning task.\n"
        . $e->getMessage()
      );
    }

    return $task_success;
  }

  /**
   * {@inheritdoc}
   */
  public function getProgress() :float {
    $data = $this->state->get(static::STATE_KEY_DATA_PREFIX . $this->id, []);

    if (empty($data)) {
      // No more data available. Assume process ended.
      $progress = 1;
    }
    else {
      // Compute progress.
      $tripal_dbx = \Drupal::service('tripal.dbx');
      $target_schema = $this->output_schemas[0];
      $db_size = $tripal_dbx->getDatabaseSize($target_schema);
      $progress = ($db_size - $data['db_size']) / $data['chado_size'];
      if (0.01 > $progress) {
        $progress = 0.01;
      }
      else if (1 <= $progress) {
        // Not done yet since we have data in state API.
        $progress = 0.99;
      }
    }
    return $progress;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatus() :string {
    $status = '';
    $progress = $this->getProgress();
    if (0.01 >= $progress) {
      $status = 'Cloning not started yet.';
    }
    elseif (1 > $progress) {
      $status = 'Cloning in progress.';
    }
    else {
      $status = 'Cloning done.';
    }
    return $status;
  }

}