tripal_chado/src/Task/ChadoInstaller.php

Summary

Maintainability
A
1 hr
Test Coverage
D
61%
<?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 installer.
 *
 * Usage:
 * @code
 * // Where 'chado' is the name of the Chado schema to instanciate.
 * $installer = \Drupal::service('tripal_chado.installer');
 * $installer->setParameters([
 *   'output_schemas' => ['chado'],
 *   'version' => '1.3',
 * ]);
 * if (!$installer->performTask()) {
 *   // Display a message telling the user the task failed and details are in
 *   // the site logs.
 * }
 * @endcode
 */
class ChadoInstaller extends ChadoTaskBase {

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

  /**
   * Default version.
   */
  public const DEFAULT_CHADO_VERSION = '1.3';

  /**
   * The number of chunk files per version we can install.
   *
   * @todo: We should use one single SQL file.
   */
  protected $installNumChunks = [
    '1.3' => 41,
  ];

  /**
   * Validate task parameters.
   *
   * Parameter array provided to the class constructor must include one output
   * schema and it may include a version number:
   * ```
   * ['output_schemas' => ['chado'], 'version' => '1.3']
   * ```
   *
   * @throws \Drupal\tripal_biodb\Exception\ParameterException
   *   A descriptive exception is thrown in cas of invalid parameters.
   */
  public function validateParameters() :void {
    try {
      // Select a default version if needed.
      if (empty($this->parameters['version'])) {
        $this->parameters['version'] = static::DEFAULT_CHADO_VERSION;
      }
      // Check the version passed in is not an array or object.
      if (is_array($this->parameters['version'])
        || is_object($this->parameters['version'])) {

        throw new ParameterException(
          "The requested version must be a string; whereas, you passed an
          array or object: " . print_r($this->parameters['version'], TRUE)
        );
      }
      // If the version is not a string then make it so...
      if (!is_string($this->parameters['version'])) {
        $this->parameters['version'] = strval($this->parameters['version']);
      }
      // Check the version is valid.
      if (!array_key_exists(
          $this->parameters['version'],
          $this->installNumChunks
        )
      ) {
        throw new ParameterException(
          "That requested version ("
          . $this->parameters['version']
          . ") is not supported by this installer."
        );
      }

      // Check input.
      if (!empty($this->parameters['input_schemas'])) {
        throw new ParameterException(
          "Chado installer does not take input schemas. Only one output 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."
        );
      }
      $tripal_dbx = \Drupal::service('tripal.dbx');
      $output_schema = $this->outputSchemas[0];

      // 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 name is not reserved.
      $issue = $tripal_dbx->isInvalidSchemaName($output_schema->getSchemaName());
      if ($issue) {
        throw new ParameterException($issue);
      }
    }
    catch (\Exception $e) {
      // Log.
      $this->logger->error($e->getMessage());
      // Rethrow.
      throw $e;
    }
  }

  /**
   * Installs a given chado schema version into the specified schema.
   *
   * The install procedure uses a set of SQL files.
   *
   * Task parameter array provided to the class constructor includes:
   * - 'output_schemas' array: one output schema that must not exist (required)
   * - 'input_schemas' array: no input schema
   * - 'version' string: a version number (optional, default to
   *   ::DEFAULT_CHADO_VERSION)
   *
   * Example:
   * ```
   * ['output_schemas' => ['chado'], 'version' => '1.3']
   * ```
   *
   * @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
    {
      $target_schema = $this->outputSchemas[0];
      $tripal_dbx = \Drupal::service('tripal.dbx');

      // Save task initial data for later progress computation.
      // @todo: We should use one single SQL file.
      $data = ['progress' => 0];
      $this->state->set(static::STATE_KEY_DATA_PREFIX . $this->id, $data);

      // Install schema.
      $version = $this->parameters['version'];
      $num_chunks = $this->installNumChunks[$version];
      // 1) Drop the common schemas if they already exist.
      // We do not need to drop Chado schema since it is a parameter requirement
      // that the schema must not exist already.
      // @todo: The Chado SQL file should use 'IF NOT EXISTS' everywhere
      // possible to avoid touching those schemas.
      if ($tripal_dbx->schemaExists('genetic_code')) {
        $tripal_dbx->dropSchema('genetic_code');
      }
      if ($tripal_dbx->schemaExists('so')) {
        $tripal_dbx->dropSchema('so');
      }
      if ($tripal_dbx->schemaExists('frange')) {
        $tripal_dbx->dropSchema('frange');
      }

      // 2) Create the schema.
      $target_schema->schema()->createSchema();

      // 3) Apply SQL files containing table definitions.
      $module_path = \Drupal::service('extension.list.module')
        ->getPath('tripal_chado');
      $path = $module_path . '/chado_schema/parts-v' . $version . '/';
      for ($i = 1; $i <= $num_chunks; $i++) {
        $file = $path . 'default_schema-' . $version . '.part' . $i . '.sql';
        $success = $target_schema->executeSqlFile(
          $file,
          ['chado' => $target_schema->getSchemaName(),]
        );

        if (!$success) {
          throw new TaskException(
            "Schema installation part $i of $num_chunks Failed...\nInstallation (Step 1 of 2) problems!"
          );
        }
        $this->logger->info("Import part $i of $num_chunks Successful!");
        $data = ['progress' => ($i/$num_chunks)*0.80];
        $this->state->set(static::STATE_KEY_DATA_PREFIX . $this->id, $data);
      }
      $this->logger->info("Install of Chado v1.3 (Step 1 of 3) successful.");

      // 4) Initialize the schema with basic data.
      $init_file =
        $module_path
        . '/chado_schema/initialize-'
        . $version
        . '.sql'
      ;
      $success = $target_schema->executeSqlFile(
        $init_file,
        ['chado' => $target_schema->getSchemaName(),]
      );
      if (!$success) {
        throw new TaskException("Installation (Step 2 of 3) problems!");
      }
      $this->logger->info("Install of Chado v1.3 (Step 2 of 3) successful.");
      $data = ['progress' => 0.90];
      $this->state->set(static::STATE_KEY_DATA_PREFIX . $this->id, $data);

      // 5) Finally set the version and tell Tripal.
      $vsql = "
        INSERT INTO {1:chadoprop} (type_id, value)
          VALUES (
           (SELECT cvt.cvterm_id
            FROM {1:cvterm} cvt
              INNER JOIN {1:cv} cv ON cvt.cv_id = cv.cv_id
             WHERE cv.name = 'chado_properties' AND cvt.name = 'version'),
           :version)
      ";
      $target_schema->query($vsql, [':version' => $version]);
      $data = ['progress' => 0.95];
      $this->state->set(static::STATE_KEY_DATA_PREFIX . $this->id, $data);
      $this->connection
        ->insert('chado_installations')
        ->fields([
          'schema_name' => $target_schema->getSchemaName(),
          'version' => $version,
          'created' => \Drupal::time()->getRequestTime(),
          'updated' => \Drupal::time()->getRequestTime(),
        ])
        ->execute()
      ;
      $data = ['progress' => 1.];
      $this->state->set(static::STATE_KEY_DATA_PREFIX . $this->id, $data);

      // 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 Chado installation 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 {
      $progress = $data['progress'];
    }
    return $progress;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatus() :string {
    $status = '';
    $progress = $this->getProgress();
    if (0.8 >= $progress) {
      $status = 'Installation (Step 1 of 3)';
    }
    elseif (0.9 >= $progress) {
      $status = 'Installation (Step 2 of 3)';
    }
    elseif (1 > $progress) {
      $status = 'Installation (Step 3 of 3)';
    }
    else {
      $status = 'Installation done.';
    }
    return $status;
  }

}