Browse Source

Converting web services to classes

Stephen Ficklin 7 years ago
parent
commit
96ab976a55

+ 1 - 1
legacy/tripal_feature/views_handlers/views_handler_field_residues.inc

@@ -17,7 +17,7 @@ class views_handler_field_residues extends views_handler_field {
    */
   function construct() {
     parent::construct();
-      $this->additional_fields['residues'] = array('table' => 'feature', 'field' => 'residues');
+    $this->additional_fields['residues'] = array('table' => 'feature', 'field' => 'residues');
   }
 
   /**

+ 1 - 1
tripal/includes/TripalFields/TripalField.inc

@@ -94,7 +94,7 @@ class TripalField {
 
 
   // --------------------------------------------------------------------------
-  //                     CONSTRUCTORS
+  //                     CONSTRUCTOR
   // --------------------------------------------------------------------------
 
   /**

+ 56 - 0
tripal_ws/api/tripal_ws.api.inc

@@ -46,4 +46,60 @@ function hook_tripal_ws_value(&$items, $field, $instance) {
       }
     }
   }
+}
+
+/**
+ * Retrieves a list of TripalWebService implementations.
+ *
+ * The TripalWebService classes can be added by a site developer that wishes
+ * to create a new Tripal compatible web serivce.  The class file should
+ * be placed in the [module]/includes/TripalWebService directory.  Tripal will
+ * support any service as long as it is in this directory and extends the
+ * TripalWebService class.
+ *
+ * @return
+ *   A list of TripalWebService names.
+ */
+function tripal_get_web_services() {
+  $services = array();
+
+  $modules = module_list(TRUE);
+  foreach ($modules as $module) {
+    // Find all of the files in the tripal_chado/includes/fields directory.
+    $service_path = drupal_get_path('module', $module) . '/includes/TripalWebService';
+    $service_files = file_scan_directory($service_path, '/.inc$/');
+    // Iterate through the fields, include the file and run the info function.
+    foreach ($service_files as $file) {
+      $class = $file->name;
+      module_load_include('inc', $module, 'includes/TripalWebService/' . $class);
+      if (class_exists($class) and is_subclass_of($class, 'TripalWebService')) {
+        $services[] = $class;
+      }
+    }
+  }
+  return $services;
+}
+
+/**
+ * Loads the TripalWebService class file into scope.
+ *
+ * @param $class
+ *   The TripalWebService class to include.
+ *
+ * @return
+ *   TRUE if the field type class file was found, FALSE otherwise.
+ */
+function tripal_load_include_web_service_class($class) {
+
+  $modules = module_list(TRUE);
+  foreach ($modules as $module) {
+    $file_path = realpath(".") . '/' . drupal_get_path('module', $module) . '/includes/TripalWebService/' . $class . '.inc';
+    if (file_exists($file_path)) {
+      module_load_include('inc', $module, 'includes/TripalWebService/' . $class);
+      if (class_exists($class)) {
+        return TRUE;
+      }
+    }
+  }
+  return FALSE;
 }

+ 0 - 49
tripal_ws/includes/TripalContentTypeService.inc

@@ -1,49 +0,0 @@
-<?php
-
-class TripalContentTypeService extends TripalWebService {
-
-  public static $label = 'Content Types';
-  public static $description = 'Provides acesss to the biological and ' .
-    'ancilliary data available on this site. Each content type represents ' .
-    'biological data that is defined in a controlled vocabulary (e.g. ' .
-    'Sequence Ontology term: gene (SO:0000704)).'
-  public static $name = 'content';
-
-  public function __construct() {
-    parent::__construct();
-
-    // Iterate through all of the entitie types (bundles) and add them as
-    // supported classes.
-    $bundles = db_select('tripal_bundle', 'tb')
-      ->fields('tb')
-      ->orderBy('tb.label', 'ASC')
-      ->execute();
-
-    // Iterate through the terms and add an entry in the collection.
-    $i = 0;
-    while ($bundle = $bundles->fetchObject()) {
-      $entity =  entity_load('TripalTerm', array('id' => $bundle->term_id));
-      $term = reset($entity);
-      $vocab = $term->vocab;
-
-      // Get the bundle description. If no description is provided then
-      // use the term definition
-      $description = tripal_get_bundle_variable('description', $bundle->id);
-      if (!$description) {
-        $description = $term->definition;
-      }
-      // Add the bundle as a content type.
-//       $response['member'][] = array(
-//         '@id' => url($api_url . '/content/' . urlencode($bundle->label), array('absolute' => TRUE)),
-//         '@type' => $term->name,
-//         'label' => $bundle->label,
-//         'description' => $description,
-//       );
-
-      $operations = array();
-      $properties = array();
-      $this->addSupportedClass($term->name, $bundle->label, $description, $operations, $properties);
-      $this->addContextItem($term->name, $term->url);
-    }
-  }
-}

+ 0 - 1
tripal_ws/includes/TripalVocabService.inc

@@ -1 +0,0 @@
-<?php

+ 238 - 37
tripal_ws/includes/TripalWebService.inc

@@ -2,65 +2,266 @@
 
 class TripalWebService {
 
-  public static $label;
-  public static $description;
-  public static $version;
-  public static $name;
+  // --------------------------------------------------------------------------
+  //                     EDITABLE STATIC CONSTANTS
+  //
+  // The following constants SHOULD be set for each descendent class.  They are
+  // used by the static functions to provide information to Drupal about
+  // the field and it's default widget and formatter.
+  // --------------------------------------------------------------------------
+  /**
+   * The human-readable label for this web service.
+   */
+  public static $label = 'Base WebService';
+  /**
+   * A bit of text to describe what this service provides.
+   */
+  public static $description = 'This is the base class for Tripal web services as is not meant to be used on it\'s own';
+  /**
+   * A machine-readable type for this service. This name must be unique
+   * among all Tripal web services and is used to form the URL to access
+   * this service.
+   */
+  public static $type = 'services';
 
-  private $context;
-  private $response;
-  private $supportedClasses;
 
+  // --------------------------------------------------------------------------
+  //              PROTECTED CLASS MEMBERS -- DO NOT OVERRIDE
+  // --------------------------------------------------------------------------
+  /**
+   * The resource that will be returned by the webservice given the
+   * arguments provided.  This is a private
+   */
+  protected $resource;
 
   /**
-   *
+   *   An array containing the elements of the URL path. Each level of the
+   *   URL appears in a separate element of the array. The service type and
+   *   version are automatically removed from the array.
+   */
+  protected $path;
+
+  /**
+   * The set of paramters provided to the sesrvice. These are the values
+   * that would occur in a URL after the question mark in an HTTP GET or
+   * the data items of an HTTP POST.
+   */
+  protected $params;
+
+  // --------------------------------------------------------------------------
+  //                             CONSTRUCTORS
+  // --------------------------------------------------------------------------
+  /**
+   * Implements the constructor.
    */
   public function __construct() {
-    $this->context = array();
-    $this->response = array();
-    $this->supportedClasses = array();
-    $this->possibleStates = array();
+    $this->resource = new TripalWebServiceResource();
+    $this->path = array();
+    $this->params = array();
+  }
+
+  // --------------------------------------------------------------------------
+  //                          OVERRIDEABLE FUNCTIONS
+  // --------------------------------------------------------------------------
 
-    // First, add the RDFS and Hydra vocabularies to the context.  All
-    // web services should use these.
-    $this->addContextItem('rdfs', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
-    $this->addContextItem('hydra', 'http://www.w3.org/ns/hydra/core#');
 
+  /**
+   * Responds to the request argument provided to the service.
+   *
+   * This function should be implemented by a TripalWebService child class.
+   *
+   */
+  public function handleRequest() {
+    // TODO: make sure the $this->path and $this->params are set before
+    // continuing.
   }
 
+  /**
+   * Retrieves the path for the current resource.
+   *
+   * This web service class always provides a resource that fits within the
+   * type of class.  The URL for each resource is needed for building the
+   * full IRI. This path can be concatenated to the Tripal webservices path to
+   * form the complete IRI for the current resource.
+   */
+  public function getResourcePath() {
+    $class = get_class($this);
+    return $class::$type;
+  }
 
-  public function addSupportedClass($type, $title, $description, $operations = array(), $properties = array()) {
-    // TODO: add some checks.
+  // --------------------------------------------------------------------------
+  //                     CLASS FUNCTIONS -- DO NOT OVERRIDE
+  // --------------------------------------------------------------------------
+  /**
+   * Sets the URL path for the resource being called.
+   *
+   * @param $path
+   *   An array containing the elements of the URL path. Each level of the
+   *   URL appears in a separate element of the array. The service type and
+   *   version are automatically removed from the array. For example, a
+   *   URL of the type http://localhost/web-services/content/v0.1/Gene/sequence
+   *   will result in a $path array containing the following:
+   *   @code
+   *     array(
+   *       'Gene',
+   *       'sequence',
+   *     );
+   *   @endcode
+   *
+   * @param unknown $path
+   */
+  public function setPath($path) {
+    $this->path = $path;
+  }
+  /**
+   * Sets the parameters for the resource.
+   *
+   * @param $params
+   *   The set of paramters provided to the sesrvice. These are the values
+   *   that would occur in a URL after the question mark in an HTTP GET or
+   *   the data items of an HTTP POST.
+   */
+  public function setParams($params) {
+    $this->params = $params;
+  }
 
-    $this->supportedClasses[] = array(
+  /**
+   * Retrieves the version number for this web service.
+   *
+   * Each web service must have version number built into the name of the
+   * class. The version number appears at the end of the class name, begins
+   * with a lower-case 'v' and is followed by two numbers (major and minor) that
+   * are separated by an underscore.  This function identifies the version
+   * from the class name and returns it here in a human-readable format.
+   *
+   * @param $sanatize
+   *   Set to TRUE to convert the period to underscore.
+   *
+   * @return
+   *   The version number for this web service.
+   */
+  public function getVersion($sanatize = FALSE) {
+
+    $class = get_class($this);
+    $major_version = '';
+    $minor_version = '';
+
+    if (preg_match('/v(\d+)_(\d+)$/', $class, $matches)) {
+      $major_version = $matches[1];
+      $minor_version = $matches[2];
+      return 'v' . $major_version . '.' . $minor_version;
+    }
+    return '';
+  }
+
+
+  /**
+   * Retrieves the context section of the response.
+   *
+   * The JSON-LD response constists of two sections the '@context' section
+   * and the data section.  This function only returns the context section
+   * of the response.
+   *
+   * @return
+   *   An associative array containing the context section of the response.
+   */
+  public function getContext() {
+    return $this->resource->getContext();
+  }
+
+
+
+  /**
+   * Returns the full web service response.
+   *
+   * The response includes both the @context and data sections of the
+   * JSON-LD response.
+   *
+   * @return
+   *   An associative array containing that can be converted to JSON.
+   */
+  public function getResponse() {
+
+    $context = $this->resource ? $this->resource->getContext() : array();
+    $type = $this->resource ? $this->resource->getType() : 'unknown';
+    $json_ld = array(
+      '@context' => $context,
+      '@id' => '',
       '@type' => $type,
-      'hydra:title' => $title,
-      'hydra:description' => $description,
-      'supportedOperation' => $operations,
-      'supportedProperty' => $properties,
     );
+
+    // Get the data array and set the IRIs fore each ID.
+    $data = $this->getData();
+    $this->setIRI($data);
+
+    return array_merge($json_ld, $data);
   }
 
+  /**
+   * A recursive function for setting the IRI (i.e. JSON-LD @id property).
+   *
+   * @param $data
+   *   An array of data as returned by the $this->getData() function
+   */
+  protected function setIRI(&$data) {
+    global $base_url;
 
-  public function getSupportedClasses() {
-    return $this->supportedClasses;
+    if(array_key_exists('@id', $data)) {
+      $class = get_class($this);
+      $version = $this->getVersion();
+      $type = $class::$type;
+      $data['@id'] = $base_url . '/web-services/' . $type . '/' . $version . '/' . $data['@id'];
+    }
+    foreach ($data as $key => $val) {
+      if (is_array($val)) {
+        $this->setIRI($data[$key]);
+      }
+    }
   }
+  /**
+   * Retrieves the data section of the response.
+   *
+   * The JSON-LD response constists of two sections the '@context' section
+   * and the data section.  This function only returns the data section
+   * of the response.
+   *
+   * @return
+   *   An associative array containing the data section of the response.
+   */
+  public function getData() {
 
-  public function addContextItem($name, $details) {
-    $this->context[$name] = $details;
+    if ($this->resource) {
+      return $this->resource->getData();
+    }
+    return array();
   }
 
-  public function getContext() {
-    return $this->context;
+  /**
+   * Sets the resource to be returned by this web service.
+   *
+   * @param $resource.
+   *   An implementation of a TripalWebServiceResource.
+   */
+  public function setResource($resource) {
+    // Make sure the $servcie provides is a TripalWebServcie class.
+    if (!is_a($resource, 'TripalWebServiceResource')) {
+      throw new Exception("Cannot add a new resource to this web service as it is not a TripalWebServiceResource.");
+    }
+
+    $this->resource = $resource;
   }
 
-  public function getDocumentation() {
-     return array(
-       '@context' => $this->getContext(),
-       '@id' => '',
-       '@type' => 'ApiDocumentation',
-       'supportedClass' => $this->getSupportedClasses(),
-       'supportedStatus' => $this->getSuportedStatus(),
-     );
+
+
+  /**
+   * Set an error for the service.
+   *
+   * @param $message
+   *   The error message to report.
+   */
+  public function setError($message) {
+    $this->resource = new TripalWebServiceResource();
+    $this->resource->addContextItem('error', 'rdfs:error');
+    $this->resource->addProperty('error', $message);
   }
 }

+ 19 - 0
tripal_ws/includes/TripalWebService/TripalDocService_V0_1.inc

@@ -0,0 +1,19 @@
+<?php
+
+class TripalDocService_v0_1 extends TripalWebService {
+
+  /**
+   * The human-readable label for this web service.
+   */
+  public static $label = 'API Documentation';
+  /**
+   * A bit of text to describe what this service provides.
+   */
+  public static $description = 'Provides documentation for the use of this web services.';
+  /**
+   * A machine-readable type for this service. This name must be unique
+   * among all Tripal web services and is used to form the URL to access
+   * this service.
+   */
+  public static $type = 'doc';
+}

+ 317 - 0
tripal_ws/includes/TripalWebService/TripalEntityService_v0_1.inc

@@ -0,0 +1,317 @@
+<?php
+
+class TripalEntityService_v0_1 extends TripalWebService {
+
+  /**
+   * The human-readable label for this web service.
+   */
+  public static $label = 'Content Types';
+  /**
+   * A bit of text to describe what this service provides.
+   */
+  public static $description = 'Provides acesss to the biological and ' .
+    'ancilliary data available on this site. Each content type represents ' .
+    'biological data that is defined in a controlled vocabulary (e.g. ' .
+    'Sequence Ontology term: gene (SO:0000704)).';
+  /**
+   * A machine-readable type for this service. This name must be unique
+   * among all Tripal web services and is used to form the URL to access
+   * this service.
+   */
+  public static $type = 'content';
+
+
+  /**
+   * Implements the constructor
+   */
+  public function __construct() {
+    parent::__construct();
+
+  }
+
+  /**
+   * @see TripalWebService::handleRequest()
+   */
+  public function handleRequest($path) {
+
+    // Get the content type.
+    $ctype     = (count($this->path) > 0) ? $this->path[0] : '';
+    $entity_id = (count($this->path) > 1) ? $this->path[1] : '';
+
+    // If we have no content type then list all of the available content types.
+    if ($ctype and !$entity_id) {
+      $this->doContentType($ctype);
+    }
+    // If we don't have an entity ID then show a paged list of entities with
+    // the given type.
+    else if ($ctype and !$entity_id) {
+    }
+    // If we have a content type and an entity ID then show the entity
+    else {
+      $this->doAllTypesList();
+    }
+  }
+
+  /**
+   * Creates a collection of resources for a given type.
+   */
+  private function doContentType($ctype) {
+    $this->resource = new TripalWebServiceCollection();
+    $this->resource->addContextItem('label', 'rdfs:label');
+
+    // Get the TripalBundle, TripalTerm and TripalVocab type for this type.
+    $bundle = tripal_load_bundle_entity(array('label' => $ctype));
+    $term = entity_load('TripalTerm', array('id' => $bundle->term_id));
+    $term = reset($term);
+
+    // Set the label for this collection.
+    $this->resource->addContextItem($term->name, $term->url);
+    $this->resource->addProperty('label', $bundle->label . " collection");
+
+    // Iterate through the fields and create a $field_mapping array that makes
+    // it easier to determine which filter criteria belongs to which field. The
+    // key is the label for the field and the value is the field name. This way
+    // user's can use the field label or the field name to form a query.
+    $field_mapping = array();
+    $fields = field_info_fields();
+    foreach ($fields as $field) {
+      if (array_key_exists('TripalEntity', $field['bundles'])) {
+        foreach ($field['bundles']['TripalEntity'] as $bundle_name) {
+          if ($bundle_name == $bundle->name) {
+            $instance = field_info_instance('TripalEntity', $field['field_name'], $bundle_name);
+            if (array_key_exists('term_accession', $instance['settings'])){
+              $vocabulary = $instance['settings']['term_vocabulary'];
+              $accession = $instance['settings']['term_accession'];
+              $term = tripal_get_term_details($vocabulary, $accession);
+              $key = $term['name'];
+              $key = strtolower(preg_replace('/ /', '_', $key));
+              $field_mapping[$key] = $field['field_name'];
+              $field_mapping[$field['field_name']] = $field['field_name'];
+            }
+          }
+        }
+      }
+    }
+
+    // Convert the filters to their field names
+    $new_params = array();
+    $order = array();
+    $order_dir = array();
+    $URL_add = array();
+    foreach ($params as $param => $value) {
+      $URL_add[] = "$param=$value";
+
+      // Ignore non filter parameters
+      if ($param == 'page' or $param == 'limit') {
+        continue;
+      }
+
+      // Handle order separately
+      if ($param == 'order') {
+        $temp = explode(',', $value);
+        foreach ($temp as $key) {
+          $matches = array();
+          $dir = 'ASC';
+          // The user can provide a direction by separating the field key and the
+          // direction with a '|' character.
+          if (preg_match('/^(.*)\|(.*)$/', $key, $matches)) {
+            $key = $matches[1];
+            if ($matches[2] == 'ASC' or $matches[2] == 'DESC') {
+              $dir = $matches[2];
+            }
+            else {
+              // TODO: handle error of providing an incorrect direction.
+            }
+          }
+          if (array_key_exists($key, $field_mapping)) {
+            $order[$field_mapping[$key]] = $key;
+            $order_dir[] = $dir;
+          }
+          else {
+            // TODO: handle error of providing a non existing field name.
+          }
+        }
+        continue;
+      }
+
+      // Break apart any operators
+      $key = $param;
+      $op = '=';
+      $matches = array();
+      if (preg_match('/^(.+);(.+)$/', $key, $matches)) {
+        $key = $matches[1];
+        $op = $matches[2];
+      }
+
+      // Break apart any subkeys and pull the first one out for the term name key.
+      $subkeys = explode(',', $key);
+      if (count($subkeys) > 0) {
+        $key = array_shift($subkeys);
+      }
+      $column_name = $key;
+
+      // Map the values in the filters to their appropriate field names.
+      if (array_key_exists($key, $field_mapping)) {
+        $field_name = $field_mapping[$key];
+        if (count($subkeys) > 0) {
+          $column_name .= '.' . implode('.', $subkeys);
+        }
+        $new_params[$field_name]['value'] = $value;
+        $new_params[$field_name]['op'] = $op;
+        $new_params[$field_name]['column'] = $column_name;
+      }
+      else {
+        throw new Exception("The filter term, '$key', is not available for use.");
+      }
+    }
+
+    // Get the list of entities for this bundle.
+    $query = new TripalFieldQuery();
+    $query->entityCondition('entity_type', 'TripalEntity');
+    $query->entityCondition('bundle', $bundle->name);
+    foreach($new_params as $field_name => $details) {
+      $value = $details['value'];
+      $column_name = $details['column'];
+      switch ($details['op']) {
+        case 'eq':
+          $op = '=';
+          break;
+        case 'gt':
+          $op = '>';
+          break;
+        case 'gte':
+          $op = '>=';
+          break;
+        case 'lt':
+          $op = '<';
+          break;
+        case 'lte':
+          $op = '<=';
+          break;
+        case 'ne':
+          $op = '<>';
+          break;
+        case 'contains':
+          $op = 'CONTAINS';
+          break;
+        case 'starts':
+          $op = 'STARTS WITH';
+          break;
+        default:
+          $op = '=';
+      }
+      // We pass in the $column_name as an identifier for any sub fields
+      // that are present for the fields.
+      $query->fieldCondition($field_name, $column_name, $value, $op);
+    }
+
+    // Perform the query just as a count first to get the number of records.
+    $cquery = clone $query;
+    $cquery->count();
+    $num_records = $cquery->execute();
+    $num_records = count($num_records['TripalEntity']);
+
+    if (!$num_records) {
+      $num_records = 0;
+    }
+
+    // Add in the pager to the response.
+    $response['totalItems'] = $num_records;
+    $limit = array_key_exists('limit', $params) ? $params['limit'] : 25;
+
+    $total_pages = ceil($num_records / $limit);
+    $page = array_key_exists('page', $params) ? $params['page'] : 1;
+
+    // Set the query order
+    $order_keys = array_keys($order);
+    for($i = 0; $i < count($order_keys); $i++) {
+      $query->fieldOrderBy($order_keys[$i], $order[$order_keys[$i]], $order_dir[$i]);
+    }
+
+    // Set the query range
+    $start = ($page - 1) * $limit;
+    $query->range($start, $limit);
+
+    // Now perform the query.
+    $results = $query->execute();
+
+    //$this->resource->initPager($num_records, $params['limit']);
+    $this->resource->initPager($num_records, 25);
+
+    // Iterate through the entities and add them to the list.
+    $i = 0;
+    foreach ($results['TripalEntity'] as $entity_id => $stub) {
+      $vocabulary = '';
+      $term_name = '';
+
+      // We don't need all of the attached fields for an entity so, we'll
+      // not use the entity_load() function.  Instead just pull it from the
+      // database table.
+      $query = db_select('tripal_entity', 'TE');
+      $query->join('tripal_term', 'TT', 'TE.term_id = TT.id');
+      $query->fields('TE');
+      $query->fields('TT', array('name'));
+      $query->condition('TE.id', $entity_id);
+      $entity = $query->execute()->fetchObject();
+
+      //$entity = tripal_load_entity('TripalEntity', array($entity->id));
+      $member = new TripalWebServiceResource();
+      $member->addContextItem('label', 'rdfs:label');
+      $member->addContextItem('itemPage', 'schema:itemPage');
+      $member->setID($entity->id);
+      $member->addProperty('label', $entity->title);
+      $member->addProperty('itemPage', url('/bio_data/' . $entity->id, array('absolute' => TRUE)));
+
+      $this->resource->addMember($member);
+      $i++;
+    }
+  }
+
+  /**
+   * Creates a resources that contains the list of content types.
+   */
+  private function doAllTypesList() {
+    $this->resource = new TripalWebServiceCollection();
+    $this->resource->addContextItem('label', 'rdfs:label');
+    $this->resource->addProperty('label', 'Content Types');
+
+    // Get the list of published terms (these are the bundle IDs)
+    $bundles = db_select('tripal_bundle', 'tb')
+      ->fields('tb')
+      ->orderBy('tb.label', 'ASC')
+      ->execute();
+
+    // Iterate through the terms and add an entry in the collection.
+    $i = 0;
+    while ($bundle = $bundles->fetchObject()) {
+      $entity =  entity_load('TripalTerm', array('id' => $bundle->term_id));
+      $term = reset($entity);
+      $vocab = $term->vocab;
+
+      // Get the bundle description. If no description is provided then
+      // use the term definition
+      $description = tripal_get_bundle_variable('description', $bundle->id);
+      if (!$description) {
+        $description = $term->definition;
+      }
+
+      $member = new TripalWebServiceResource();
+      $member->addContextItem($term->name, $term->url);
+      $member->addContextItem('label', 'rdfs:label');
+      $member->addContextItem('description', 'hydra:description');
+      $member->setID(urlencode($bundle->label));
+      $member->setType($term->name);
+      $member->addProperty('label', $bundle->label);
+      $member->addProperty('description', $description);
+      $this->resource->addMember($member);
+
+    }
+  }
+  /**
+   * @see TripalWebService::getResourcePath()
+   */
+  public function getResourcePath() {
+    $base_path = parent::getResourcePath();
+    return $base_path . '/' . $this->resource['@type'];
+  }
+}

+ 19 - 0
tripal_ws/includes/TripalWebService/TripalVocabService_v0_1.inc

@@ -0,0 +1,19 @@
+<?php
+
+class TripalVocabService_v0_1 extends TripalWebService {
+
+  /**
+   * The human-readable label for this web service.
+   */
+  public static $label = 'Vocabulary';
+  /**
+   * A bit of text to describe what this service provides.
+   */
+  public static $description = 'Provides access to vocabulary terms that are in use by this site.';
+  /**
+   * A machine-readable type for this service. This name must be unique
+   * among all Tripal web services and is used to form the URL to access
+   * this service.
+   */
+  public static $type = 'vocabulary';
+}

+ 164 - 0
tripal_ws/includes/TripalWebServiceCollection.inc

@@ -0,0 +1,164 @@
+<?php
+
+class TripalWebServiceCollection extends TripalWebServiceResource {
+
+  /**
+   * Holds the data portion of the JSON-LD response if this resource is
+   * a collection.
+   */
+  protected $members;
+
+  /**
+   * Set to TRUE if paging should be enabled.  Paging is disabled by default.
+   */
+  protected $doPaging;
+
+  /**
+   * The total number of items.  This variable is used if paging is turned on.
+   * Otherwise it's ignored and the total number of items reported by the
+   * collection will be the number of elements in the $members array.
+   */
+  protected $totalItems;
+
+  /**
+   * When the collection contains more than the itemsPerpage amount then
+   * the results will be paged.
+   */
+  protected $itemsPerPage;
+
+
+  /**
+   * Implements the constructor.
+   *
+   * @param TripalWebService $service
+   *   An instance of a TripalWebService or class that extends it.
+   */
+  public function __construct() {
+    parent::__construct();
+    $this->members = array();
+    $this->addContextItem('Collection', "hydra:Collection");
+    $this->addContextItem('totalItems', "hydra:totalItems");
+    $this->addContextItem('member', "member");
+    parent::setType('Collection');
+
+    // If the totalItems is set to -1 then this means paging is turned off and
+    // all of the elements in the $memgbers array should be used.
+    $this->totalItems = 0;
+    $this->itemsPerPage = 25;
+    $this->doPaging = FALSE;
+  }
+
+  /**
+   * Initializes the pager.
+   *
+   * @param $totalItems
+   *   The total number of items available.
+   * @param $itemsPerPage
+   *   The maximum number of items per page.
+   */
+  public function initPager($totalItems, $itemsPerPage) {
+    $this->doPaging = TRUE;
+    $this->totalItems = $totalItems;
+    $this->itemsPerPage = $itemsPerPage;
+  }
+
+  /**
+   * Adds a new member to this resource if it is a collection.
+   *
+   * @param $member
+   *   A TripalWebServiceResource member whose type is the same as this
+   *   resource
+   */
+  public function addMember($member) {
+    // Make sure the $servcie provides is a TripalWebServcie class.
+    if (!is_a($member, 'TripalWebServiceResource')) {
+      throw new Exception("Cannot add a new member to this resource collection as it is not a TripalWebServiceResource.");
+    }
+    $this->members[] = $member;
+  }
+
+  /**
+   * @see TripalWebServiceResource::setType()
+   */
+  public function setType($type) {
+    throw new Exception("The type for a Collection can only be collection.");
+  }
+
+  /**
+   * Retrieves the data section of the resource.
+   *
+   * The JSON-LD response constists of two sections the '@context' section
+   * and the data section.  This function only returns the data section
+   * for this resource
+   *
+   * @return
+   *   An associative array containing the data section of the response.
+   */
+  public function getData() {
+    $data = $this->data;
+    $data['totalItems'] = 0;
+
+    if ($this->doPaging == TRUE) {
+
+      $data['totalItems'] = $this->totalItems;
+      $total_pages = ceil($this->totalItems / $this->itemsPerPage);
+      $page = array_key_exists('page', $this->params) ? $this->params['page'] : 1;
+      $limit = $this->itemsPerPage;
+
+      if ($this->totalItems > 0) {
+        $data['view'] = array(
+          '@id' => '?' . implode('&', array_merge(array("page=$page", "limit=$limit"))),
+          '@type' => 'PartialCollectionView',
+          'first' => '?' . implode('&', array_merge(array("page=1", "limit=$limit"))),
+          'last' => '?' . implode('&', array_merge(array("page=$total_pages", "limit=$limit"))),
+        );
+        $prev = $page - 1;
+        $next = $page + 1;
+        if ($prev > 0) {
+          $data['view']['previous'] = $URL . '?' . implode('&', array_merge($URL_add, array("page=$prev", "limit=$limit")));
+        }
+        if ($next < $total_pages) {
+          $data['view']['next'] = $URL . '?' . implode('&', array_merge($URL_add, array("page=$next", "limit=$limit")));
+        }
+      }
+
+    }
+    else {
+      $data['totalItems'] = count($member_data);
+    }
+
+    $member_data = array();
+    foreach ($this->members as $key => $member) {
+      $member_data[] = $member->getData();
+    }
+    $data['members'] = $member_data;
+
+    // If paging of this collection is enabled then add the pager control links.
+
+    return $data;
+  }
+
+  /**
+   * Retrieves the data section of the resource.
+   *
+   * The JSON-LD response constists of two sections the '@context' section
+   * and the data section.  This function only returns the data section
+   * for this resource
+   *
+   * @return
+   *   An associative array containing the data section of the response.
+   */
+  public function getContext() {
+    if ($this->doPaging == TRUE) {
+      $this->addContextItem('view', 'hydra:PartialCollectionView');
+    }
+    $context = $this->context;
+    foreach ($this->members as $key => $member) {
+      $citems = $member->getContext();
+      foreach ($citems as $key => $val) {
+        $context[$key] = $val;
+      }
+    }
+    return $context;
+  }
+}

+ 0 - 19
tripal_ws/includes/TripalWebServiceProvider.inc

@@ -1,19 +0,0 @@
-<?php
-
-class TripalWebServiceProvider {
-  private $services;
-  private $possibleStatus;
-
-
-  public function __construct() {
-    $this->services = array();
-  }
-
-  public function addService($service) {
-    $this->services[] = $service;
-  }
-
-  public function getPossibleStatus() {
-    return $this->possibleStatus;
-  }
-}

+ 220 - 0
tripal_ws/includes/TripalWebServiceResource.inc

@@ -0,0 +1,220 @@
+<?php
+
+class TripalWebServiceResource {
+  /**
+   * The unique identifier for this resource.
+   */
+  protected $id;
+  /**
+   * The unique type of resource.  The type must exists in the
+   * context for the web service.
+   */
+  protected $type;
+
+  /**
+   * The JSON-LD compatible context for this resource.
+   */
+  protected $context;
+
+  /**
+   * Holds the data portion of the JSON-LD response for this resource.
+   */
+  protected $data;
+
+
+  /**
+   * Implements the constructor.
+   *
+   * @param TripalWebService $service
+   *   An instance of a TripalWebService or class that extends it.
+   */
+  public function __construct() {
+    $this->context = array();
+    $this->data = array();
+
+    // First, add the RDFS and Hydra vocabularies to the context.  All Tripal
+    // web services should use these.
+    $this->addContextItem('rdfs', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
+    $this->addContextItem('hydra', 'http://www.w3.org/ns/hydra/core#');
+
+    $this->data['@id'] = '';
+    $this->data['@type'] = '';
+  }
+
+  /**
+   * Adds a term to the '@context' section for this resource.
+   *
+   * @param $name
+   *   The term name.
+   * @param $iri
+   *   The Internationalized Resource Identifiers or it's shortcut.
+   */
+  public function addContextItem($name, $iri) {
+    // TODO: make sure that if a shortcut is used that the parent is present.
+    $this->context[$name] = $iri;
+  }
+
+  /**
+   * Removes a term for the '@context' section for this resource.
+   *
+   * @param $name
+   *   The term name.
+   * @param $iri
+   *   The Internationalized Resource Identifiers or it's shortcut.
+   */
+  public function removeContextItem($name, $iri) {
+    // TODO: make sure that if a shortcut is used that the parent is present.
+    unset($this->context[$name]);
+  }
+
+  /**
+   * Sets the resource type.
+   *
+   * The type exist in the context of the web service.
+   *
+   * @param $type
+   *   The type
+   */
+  public function setType($type) {
+    $keys = array_keys($this->context);
+    if (!in_array($type, $keys)) {
+      throw new Exception("The resource type, '$type', has not yet been added to the " .
+          "context of the web service. Use the addContextItem() function of the web service " .
+          "to add this term.");
+    }
+    $this->type = $type;
+    $this->data['@type'] = $type;
+  }
+
+  /**
+   * Set's the unique identifier for the resource.
+   *
+   * Every resource must have a unique Idientifer. In JSON-LD the '@id' element
+   * identifies the IRI for the resource which will include the unique
+   * identifier.  The TraiplWebService to which a resource is added will
+   * build the IRIs but it needs the unique ID of each resource.
+   *
+   * @param $id
+   *   The unique identifier for the resource.
+   */
+  public function setID($id) {
+    $this->id = $id;
+    $this->data['@id'] = $id;
+  }
+
+
+  /**
+   * Retrieves the unique identifier for this resource.
+   *
+   * Every resource must have a unique Idientifer. In JSON-LD the '@id' element
+   * identifies the IRI for the resource which will include the unique
+   * identifier.  The TraiplWebService to which a resource is added will
+   * build the IRIs but it needs the unique ID of each resource.
+   *
+   * @return
+   *   The unique identifier for the resource.
+   */
+  public function getID() {
+    return $this->id;
+  }
+
+  /**
+   * Retreives the type of this resource.
+   *
+   * @return
+   *   The name of the resource.
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * Adds a new key/value pair to the web serivces response.
+   *
+   * The value must either be a scalar or another TripalWebServiceResource
+   * object.
+   *
+   * @param unknown $key
+   *   The name of the $key to add. This key must already be present in the
+   *   web service context by first adding it using the addContextItem()
+   *   member function.
+   * @param unknown $value
+   *   The value of the key which must either be a scalar or a
+   *   TripalWebServiceResource instance.
+   */
+  public function addProperty($key, $value) {
+
+    // Make sure the key is already present in the context.
+    $keys = array_keys($this->context);
+    if (!in_array($key, $keys)) {
+      throw new Exception("The key, '$key', has not yet been added to the " .
+          "context. Use the addContextItem() function to add this key prior to adding a value for it.");
+    }
+    if (is_scalar($value)) {
+      $this->data[$key] = $value;
+    }
+    else if (!is_subclass_of($value, 'TripalWebServiceResource')) {
+      $this->data[$key] = $value;
+    }
+    else {
+      throw new Exception("The value must either be a scalar or a TripalWebServiceResource");
+    }
+  }
+
+  /**
+   * A recursive function that ensures all keys in an item are in the context.
+   *
+   * @param $key
+   *   The name of the current key.
+   * @param $value
+   *   The avlue assigned to the current key.
+   *
+   * @throws Exception
+   *   Throws an exception of a key is not present in the context.
+   */
+  private function checkDataItem($key, $value) {
+    // Make sure the key is already present in the context.
+    $keys = array_keys($this->context);
+    if (!in_array($key, $keys)) {
+      throw new Exception("The key, '$key', has not yet been added to the " .
+        "context. Use the addContextItem() function to add this key prior to adding a value for it.");
+    }
+    // If the value is an associative array then recurse
+    if (is_array($value)) {
+      // Check if this is an associatave array (non-integer keys).
+      if (count(array_filter(array_keys($array), 'is_string')) > 0) {
+        foreach ($value as $sub_key => $sub_value) {
+          $this->checkDataItem($sub_key, $sub_value);
+        }
+      }
+    }
+  }
+
+  /**
+   * Retrieves the data section of the resource.
+   *
+   * The JSON-LD response constists of two sections the '@context' section
+   * and the data section.  This function only returns the data section
+   * for this resource
+   *
+   * @return
+   *   An associative array containing the data section of the response.
+   */
+  public function getData() {
+    return $this->data;
+  }
+
+  /**
+   * Retrieves the data section of the resource.
+   *
+   * The JSON-LD response constists of two sections the '@context' section
+   * and the data section.  This function only returns the data section
+   * for this resource
+   *
+   * @return
+   *   An associative array containing the data section of the response.
+   */
+  public function getContext() {
+      return $this->context;
+  }
+}

+ 134 - 0
tripal_ws/tripal_ws.module

@@ -1,5 +1,10 @@
 <?php
 
+require_once  "api/tripal_ws.api.inc";
+require_once  "includes/TripalWebService.inc";
+require_once  "includes/TripalWebServiceResource.inc";
+require_once  "includes/TripalWebServiceCollection.inc";
+
 
 /**
  * Implements hook_init()
@@ -35,6 +40,13 @@ function tripal_ws_menu() {
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
   );
+  // Web Services API callbacks.
+  $items['web-services'] = array(
+    'title' => 'Tripal Web Services API',
+    'page callback' => 'tripal_ws_get_services',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
 
   $items['remote/%/%/%/%'] = array(
     'page callback' => 'tripal_ws_load_remote_entity',
@@ -91,6 +103,128 @@ function tripal_ws_menu() {
   return $items;
 }
 
+/**
+ * The callback function for all RESTful web services.
+ *
+ */
+function tripal_ws_get_services() {
+  drupal_add_http_header('Content-Type', 'application/json');
+
+  try {
+    $ws_path = func_get_args();
+    $args = $_GET;
+    unset($args['q']);
+
+    // The web services should never be cached.
+    drupal_page_is_cacheable(FALSE);
+
+    // The Tripal web services bath will be:
+    // [base_path]/web-services/[service name]/v[major_version].[minor_version]
+    $matches = array();
+    $service = '';
+    $major_version = '';
+    $minor_version = '';
+    $list_services = FALSE;
+
+    // If there is no path then we should list all of the services available.
+    if (empty($ws_path)) {
+      tripal_ws_list_services();
+      return;
+    }
+    // A service path will have the service name in $ws_path[0] and the
+    // version in $ws_path[1].  If we check that the version is correctly
+    // formatted then we can look for the service class and invoke it.
+    else if (preg_match("/^v(\d+)\.(\d+)$/", $ws_path[1], $matches)) {
+      $service_type = $ws_path[0];
+      $major_version = $matches[1];
+      $minor_version = $matches[2];
+      $service_version = 'v' . $major_version . '.' . $minor_version;
+    }
+    // If the URL doesn't match then return not found.
+    else {
+      throw new Exception("Unsupported service URL.  Web service URLs must be of the following format:  ");
+    }
+
+    // Get the service that matches the service_name
+    $service = NULL;
+    $services = tripal_get_web_services();
+    foreach ($services as $service_class) {
+      tripal_load_include_web_service_class($service_class);
+      if ($service_class::$type == $service_type) {
+        $service = new $service_class();
+        if ($service->getVersion() == $service_version) {
+          break;
+        }
+        $service = NULL;
+      }
+    }
+    // If a service was not provided then return an error.
+    if (!$service) {
+      throw new Exception('The service type, "' . $service_type . '", is not available');
+    }
+
+    // Adjust the path to remove the service type and the version.
+    $adj_path = $ws_path;
+    array_shift($adj_path);
+    array_shift($adj_path);
+
+    // Now call the service to handle the request.
+    $service->setPath($adj_path);
+    $service->setParams($params);
+    $service->handleRequest();
+    $response = $service->getResponse();
+    print drupal_json_encode($response);
+
+  }
+  catch (Exception $e) {
+    $service = new TripalWebService();
+    $service->setError($e->getMessage());
+    $response = $service->getResponse();
+    print drupal_json_encode($response);
+  }
+}
+
+/**
+ * Generates the list of services as the "home page" for Tripal web services.
+ */
+function tripal_ws_list_services() {
+  global $base_url;
+
+  // Create an instance of the TriaplWebService class and use it to build
+  // the entry point for the web serivces.
+  $service = new TripalWebService();
+
+  $services = tripal_get_web_services();
+
+  // Create the parent resource which is a collection.
+  $resource = new TripalWebServiceResource();
+  $resource->addContextItem('entrypoint', 'hydra:entrypoint');
+  $resource->setType('entrypoint');
+
+  // Now add the member to the collection
+  foreach ($services as $service_class) {
+    tripal_load_include_web_service_class($service_class);
+    $service = new $service_class();
+    $version = $service->getVersion();
+    $iri = $base_url . '/web-services/' . $service_class::$type . '/' . $version;
+    $resource->addContextItem($service_class::$type, '');
+    $resource->addProperty($service_class::$type, $iri);
+  }
+
+  // For discoverability add the document webservice.
+
+  $service->setResource($resource);
+  $response = $service->getResponse();
+
+  // Using the TripalWebService class to build this entry point was
+  // conveneint but it wasn't quite meant for this way of working with it.
+  // So make some last minute fixes before returning.
+  $response['@id'] = $iri = $base_url . '/web-services';
+  print drupal_json_encode($response);
+
+
+
+}
 /**
  * The callback function for all RESTful web services.
  *