Browse Source

Merge branch '7.x-3.x' into 7.x-3.x-dashboard

Stephen Ficklin 7 years ago
parent
commit
8b867cacb3
38 changed files with 2533 additions and 287 deletions
  1. 1 1
      legacy/tripal_feature/views_handlers/views_handler_field_residues.inc
  2. 53 85
      tripal/api/tripal.entities.api.inc
  3. 13 0
      tripal/api/tripal.fields.api.inc
  4. 54 7
      tripal/api/tripal.terms.api.inc
  5. 1 1
      tripal/includes/TripalFields/TripalField.inc
  6. 55 0
      tripal/includes/tripal.term_lookup.inc
  7. 1 0
      tripal/theme/css/tripal.css
  8. 24 14
      tripal/tripal.module
  9. 1 0
      tripal_chado/api/tripal_chado.query.api.inc
  10. 18 5
      tripal_chado/includes/TripalFields/chado_linker__prop/chado_linker__prop_widget.inc
  11. 11 10
      tripal_chado/includes/TripalFields/local__source_data/local__source_data_formatter.inc
  12. 3 3
      tripal_chado/includes/TripalFields/local__source_data/local__source_data_widget.inc
  13. 2 0
      tripal_chado/includes/tripal_chado.entity.inc
  14. 17 7
      tripal_chado/includes/tripal_chado.field_storage.inc
  15. 46 39
      tripal_chado/includes/tripal_chado.fields.inc
  16. 114 7
      tripal_chado/includes/tripal_chado.semweb.inc
  17. 27 0
      tripal_chado/includes/tripal_chado.vocab_storage.inc
  18. 122 0
      tripal_chado/tripal_chado.install
  19. 4 0
      tripal_daemon/.gitattributes
  20. 44 0
      tripal_daemon/README.md
  21. 64 0
      tripal_daemon/README.txt
  22. 115 0
      tripal_daemon/TripalDaemon.inc
  23. 82 0
      tripal_daemon/includes/tripal_daemon.blocks.inc
  24. 29 0
      tripal_daemon/theme/status_block.css
  25. 65 0
      tripal_daemon/tripal_daemon.drush.inc
  26. 9 0
      tripal_daemon/tripal_daemon.info
  27. 25 0
      tripal_daemon/tripal_daemon.module
  28. 56 0
      tripal_ws/api/tripal_ws.api.inc
  29. 0 49
      tripal_ws/includes/TripalContentTypeService.inc
  30. 0 1
      tripal_ws/includes/TripalVocabService.inc
  31. 230 38
      tripal_ws/includes/TripalWebService.inc
  32. 19 0
      tripal_ws/includes/TripalWebService/TripalDocService_V0_1.inc
  33. 648 0
      tripal_ws/includes/TripalWebService/TripalEntityService_v0_1.inc
  34. 19 0
      tripal_ws/includes/TripalWebService/TripalVocabService_v0_1.inc
  35. 174 0
      tripal_ws/includes/TripalWebServiceCollection.inc
  36. 0 19
      tripal_ws/includes/TripalWebServiceProvider.inc
  37. 254 0
      tripal_ws/includes/TripalWebServiceResource.inc
  38. 133 1
      tripal_ws/tripal_ws.module

+ 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');
   }
 
   /**

+ 53 - 85
tripal/api/tripal.entities.api.inc

@@ -312,7 +312,6 @@ function tripal_create_bundle($args, &$error = '') {
         ->execute();
     }
 
-    // Allow modules to make additions to the entity when it's created.
     $bundle = tripal_load_bundle_entity(array('name' => $bundle_name));
     $modules = module_implements('bundle_create');
     foreach ($modules as $module) {
@@ -330,49 +329,7 @@ function tripal_create_bundle($args, &$error = '') {
     // Get the bundle object.
     $bundle = tripal_load_bundle_entity(array('name' => $bundle_name));
 
-    // Allow modules to add fields to the new bundle.
-    $modules = module_implements('bundle_fields_info');
-    foreach ($modules as $module) {
-      $function = $module . '_bundle_fields_info';
-      $info = $function('TripalEntity', $bundle);
-      foreach ($info as $field_name => $details) {
-        $field_type = $details['type'];
-
-        // TODO: make sure the field term exits. If not then
-        // skip it.
-
-        // If the field already exists then skip it.
-        $field = field_info_field($details['field_name']);
-        if ($field) {
-          continue;
-        }
-
-        // Create the field.
-        $field = field_create_field($details);
-        if (!$field) {
-          tripal_set_message(t("Could not create new field: %field.",
-              array('%field' =>  $details['field_name'])), TRIPAL_ERROR);
-        }
-      }
-    }
-
-    // Allow modules to add instances to the new bundle.
-    $modules = module_implements('bundle_instances_info');
-    foreach ($modules as $module) {
-      $function = $module . '_bundle_instances_info';
-      $info = $function('TripalEntity', $bundle);
-      foreach ($info as $field_name => $details) {
-        // If the field is already attached to this bundle then skip it.
-        $field = field_info_field($details['field_name']);
-        if ($field and array_key_exists('bundles', $field) and
-            array_key_exists('TripalEntity', $field['bundles']) and
-            in_array($bundle->name, $field['bundles']['TripalEntity'])) {
-          continue;
-        }
-        // Create the field instance.
-        $instance = field_create_instance($details);
-      }
-    }
+    tripal_create_bundle_fields($bundle, $term);
 
     $modules = module_implements('bundle_postcreate');
     foreach ($modules as $module) {
@@ -547,64 +504,75 @@ function tripal_get_content_type($bundle_name) {
  *
  * @param $bundle_name
  *   The name of the bundle to refresh (e.g. bio_data_4).
+ *
+ * @return
+ *   The array of field instance names that were added.
  */
-function tripal_refresh_bundle_fields($bundle_name) {
-
-  $num_created = 0;
+function tripal_create_bundle_fields($bundle, $term) {
 
-  // Get the bundle object.
-  $bundle = tripal_load_bundle_entity(array('name' => $bundle_name));
-  if (!$bundle) {
-    tripal_report_error('tripal', TRIPAL_ERROR, "Unrecognized bundle name '%bundle'.",
-        array('%bundle' => $bundle_name));
-    return FALSE;
-  }
+  $added = array();
 
   // Allow modules to add fields to the new bundle.
   $modules = module_implements('bundle_fields_info');
+  $info = array();
   foreach ($modules as $module) {
     $function = $module . '_bundle_fields_info';
-    $info = $function('TripalEntity', $bundle);
-    foreach ($info as $field_name => $details) {
-      $field_type = $details['type'];
-
-      // If the field already exists then skip it.
-      $field = field_info_field($details['field_name']);
-      if ($field) {
-        continue;
-      }
+    $temp = $function('TripalEntity', $bundle);
+    $info = array_merge($info, $temp);
+  }
 
-      // Create the field.
-      $field = field_create_field($details);
-      if (!$field) {
-        tripal_set_message(t("Could not create new field: %field.",
-            array('%field' =>  $details['field_name'])), TRIPAL_ERROR);
-      }
+  // Allow modules to alter which fields should be attached to content
+  // types they create.
+  drupal_alter('bundle_fields_info', $info, $bundle, $term);
+
+  // Iterate through all of the fields and create them.
+  foreach ($info as $field_name => $details) {
+    $field_type = $details['type'];
+
+    // TODO: make sure the field term exits. If not then
+    // skip it.
+
+    // If the field already exists then skip it.
+    $field = field_info_field($details['field_name']);
+    if ($field) {
+      continue;
+    }
+
+    // Create the field.
+    $field = field_create_field($details);
+    if (!$field) {
+      tripal_set_message(t("Could not create new field: %field.",
+          array('%field' =>  $details['field_name'])), TRIPAL_ERROR);
     }
   }
 
   // Allow modules to add instances to the new bundle.
   $modules = module_implements('bundle_instances_info');
+  $info = array();
   foreach ($modules as $module) {
     $function = $module . '_bundle_instances_info';
-    $info = $function('TripalEntity', $bundle);
-    foreach ($info as $field_name => $details) {
-      // If the field is already attached to this bundle then skip it.
-      $field = field_info_field($details['field_name']);
-      if ($field and array_key_exists('bundles', $field) and
-          array_key_exists('TripalEntity', $field['bundles']) and
-          in_array($bundle->name, $field['bundles']['TripalEntity'])) {
-        continue;
-      }
-      // Create the field instance.
-      $instance = field_create_instance($details);
-      $num_created++;
-      drupal_set_message(t("Created field: %field", array('%field' => $info[$field_name]['label'])));
-    }
+    $temp = $function('TripalEntity', $bundle);
+    $info = array_merge($info, $temp);
   }
-  if ($num_created == 0) {
-    drupal_set_message(t("No new fields were added."));
+
+  // Allow modules to alter which fields should be attached to content
+  // types they create.
+  drupal_alter('bundle_instances_info', $info, $bundle, $term);
+
+  // Iterate through all of the field instances and create them.
+  foreach ($info as $field_name => $details) {
+    // If the field is already attached to this bundle then skip it.
+    $field = field_info_field($details['field_name']);
+    if ($field and array_key_exists('bundles', $field) and
+        array_key_exists('TripalEntity', $field['bundles']) and
+        in_array($bundle->name, $field['bundles']['TripalEntity'])) {
+      continue;
+    }
+    // Create the field instance.
+    $instance = field_create_instance($details);
+    $added[] = $field_name;
   }
+  return $added;
 }
 
 /**

+ 13 - 0
tripal/api/tripal.fields.api.inc

@@ -30,6 +30,19 @@
 function hook_field_storage_tquery($conditions, $orderBy) {
   // See the tripal_chado_field_storage_tquery() function for an example.
 }
+
+function hook_bundle_fields_info($entity_type, $bundle) {
+
+}
+function hook_bundle_instances_info($entity_type, $bundle) {
+
+}
+function hook_bundle_fields_info_alter(&$info, $bundle, $term) {
+
+}
+function hook_bundle_instances_info_alter(&$info, $bundle, $term) {
+
+}
 /**
  * Retrieves a list of TripalField types.
  *

+ 54 - 7
tripal/api/tripal.terms.api.inc

@@ -116,7 +116,26 @@ function hook_vocab_get_term($vocabulary, $accession) {
   // See the tripal_chado_vocab_get_term() function for an example.
 
 }
-
+/**
+ * Hook used by the default term storage backend to provide details for a vocab.
+ *
+ * This hook is called by the tripal_entity module to retrieve information
+ * about the vocabulary from the storage backend.  It must return an array with
+ * a set of keys.
+ *
+ * @param $vocabulary
+ *   The vocabulary of the vocabulary in which the term is found.
+ *
+ * @return
+ *   An array with at least the following keys:
+ *     - name : The full name of the vocabulary.
+ *     - short_name : The short name abbreviation for the vocabulary.
+ *     - description : A brief description of the vocabulary.
+ *     - url : (optional) A URL for the online resources for the vocabulary.
+ */
+function hook_vocab_get_vocabulary($vocabulary) {
+  // See the tripal_chado_vocab_get_vocabulary() function for an example.
+}
 /**
  * Hook used by the default term storage backend to add new terms.
  *
@@ -175,15 +194,10 @@ function tripal_add_term($details) {
   }
 }
 
+
 /**
  * Retrieves full information about a vocabulary term.
  *
- * Vocabularies are stored in a database backend.  Tripal has no requirements
- * for how terms are stored.  By default, the tripal_chado modules provides
- * storage for vocabularies and terms. This function will call the
- * hook_vocab_get_term() function for the database backend that is housing the
- * vocabularies and allow it to return the details about the term.
- *
  * @param $vocabulary
  *   The vocabulary of the vocabulary in which the term is found.
  * @param $accession
@@ -217,3 +231,36 @@ function tripal_get_term_details($vocabulary, $accession) {
     }
   }
 }
+/**
+ * Retrieves full information about a vocabulary.
+ *
+ * Vocabularies are stored in a database backend.  Tripal has no requirements
+ * for how terms are stored.  By default, the tripal_chado modules provides
+ * storage for vocabularies and terms. This function will call the
+ * hook_vocab_get_term() function for the database backend that is housing the
+ * vocabularies and allow it to return the details about the term.
+ *
+ * @param $vocabulary
+ *   The vocabulary of the vocabulary in which the term is found.
+ *
+ * @return
+ *   An array with at least the following keys:
+ *     - name : The full name of the vocabulary.
+ *     - short_name : The short name abbreviation for the vocabulary.
+ *     - description : A brief description of the vocabulary.
+ *     - url : (optional) A URL for the online resources for the vocabulary.
+ */
+function tripal_get_vocabulary_details($vocabulary) {
+  // TODO: we need some sort of administrative interface that lets the user
+  // switch to the desired vocabulary type. For now, we'll just use the
+  // first one in the list.
+  $stores = module_invoke_all('vocab_storage_info');
+  if (is_array($stores) and count($stores) > 0) {
+    $keys = array_keys($stores);
+    $module = $stores[$keys[0]]['module'];
+    $function = $module . '_vocab_get_vocabulary';
+    if (function_exists($function)) {
+      return $function($vocabulary);
+    }
+  }
+}

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

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

+ 55 - 0
tripal/includes/tripal.term_lookup.inc

@@ -25,6 +25,61 @@ function tripal_vocabulary_lookup_form_submit($form, $form_state) {
 
 }
 
+function tripal_vocabulary_lookup_page($vocabulary) {
+
+  $vocab = tripal_get_vocabulary_details($vocabulary);
+
+  // If we can't find the term then just return a message.
+  if (!$vocab) {
+    drupal_set_message('The vocabulary cannot be found on this site', 'error');
+    return '';
+  }
+
+  $headers = array();
+  $rows = array();
+  $vocab_name = $vocab['name'];
+  if ($vocab['url']) {
+    $vocab_name = l($vocab['name'], $vocab['url'], array('attributes' => array('target' => '_blank')));
+  }
+  $short_name = $vocab['short_name'];
+  $vocab_desc = $vocab['description'];
+  $rows[] = array(
+    array(
+      'data' => 'Name',
+      'header' => TRUE,
+      'width' => '20%',
+    ),
+    $vocab_name,
+  );
+  $rows[] = array(
+    array(
+      'data' => 'Short Name',
+      'header' => TRUE,
+      'width' => '20%',
+    ),
+    $short_name,
+  );
+  $rows[] = array(
+    array(
+      'data' => 'Description',
+      'header' => TRUE,
+      'width' => '20%',
+    ),
+    $vocab_desc,
+  );
+
+  $table = array(
+    'header' => $headers,
+    'rows' => $rows,
+    'attributes' => array(),
+    'sticky' => FALSE,
+    'caption' => 'Vocabulary details',
+    'colgroups' => array(),
+    'empty' => '',
+  );
+  $content =  theme_table($table);
+  return $content;
+}
 /**
  *
  * @param $vocabulary

+ 1 - 0
tripal/theme/css/tripal.css

@@ -61,6 +61,7 @@ div.messages.tripal-site-admin-only{
 .tripal-dl {
   width: 100%;
   overflow: hidden;
+  margin: 0px;
 }
 .tripal-dl dt {
   float: left;

+ 24 - 14
tripal/tripal.module

@@ -121,17 +121,6 @@ function tripal_menu() {
     'file path' => drupal_get_path('module', 'tripal'),
   );
 
-//   $items['admin/tripal/loaders/obo_loader'] = array(
-//     'title' => 'OBO Controlled Vocabulary Loader',
-//     'description' => t("Import vocabularies and terms in OBO format."),
-//     'access arguments' => array('administer tripal'),
-//     'page callback' => 'drupal_get_form',
-//     'page arguments' => array('tripal_vocabulary_import_form'),
-//     'file' => 'includes/tripal.admin.inc',
-//     'file path' => drupal_get_path('module', 'tripal'),
-//     'type' => MENU_NORMAL_ITEM,
-//   );
-
   $items['admin/tripal/extension'] = array(
     'title' => 'Extensions',
     'description' => t("Configuration and management pages for Tripal extension modules."),
@@ -286,9 +275,20 @@ function tripal_menu() {
 //     'type' => MENU_NORMAL_ITEM,
 //   );
 
-  $items['cv/lookup/%/%'] = array(
+  $items['cv/lookup/%'] = array(
     'title' => 'Vocabulary Lookup',
-    'description' => t("Provides a tool to discover controlled vocabularies and their terms used by this site."),
+    'description' => t("Provides a tool to discover controlled vocabularies"),
+    'access arguments' => array('access content'),
+    'page callback' => 'tripal_vocabulary_lookup_page',
+    'page arguments' => array(2),
+    'file' => 'includes/tripal.term_lookup.inc',
+    'file path' => drupal_get_path('module', 'tripal'),
+    'type' => MENU_CALLBACK,
+  );
+
+  $items['cv/lookup/%/%'] = array(
+    'title' => 'Vocabulary Term Lookup',
+    'description' => t("Provides a tool to discover controlled vocabularies terms used by this site."),
     'access arguments' => array('access content'),
     'page callback' => 'tripal_vocabulary_lookup_term_page',
     'page arguments' => array(2, 3),
@@ -648,7 +648,17 @@ function tripal_form_alter(&$form, $form_state, $form_id) {
 }
 
 function tripal_check_new_fields($bundle_name) {
-  tripal_refresh_bundle_fields($bundle_name);
+  $bundle = tripal_load_bundle_entity(array('name' => $bundle_name));
+  $term = tripal_load_term_entity(array('term_id' => $bundle->term_id));
+
+  $added = tripal_create_bundle_fields($bundle, $term);
+  if (count($added) == 0) {
+    drupal_set_message('No new fields were added');
+  }
+  foreach ($added as $field_name) {
+    drupal_set_message('Added field: ' . $field_name);
+  }
+
   drupal_goto("admin/structure/bio_data/manage/$bundle_name/fields");
 }
 

+ 1 - 0
tripal_chado/api/tripal_chado.query.api.inc

@@ -1318,6 +1318,7 @@ function chado_select_record($table, $columns, $values, $options = NULL) {
 
     // Require the field be in the table description.
     if (!array_key_exists($field, $table_desc['fields'])) {
+      dpm(debug_backtrace());
       tripal_report_error('tripal_chado', TRIPAL_ERROR,
         'chado_select_record: The field "%field" does not exist for the table "%table".  Cannot perform query. Values: %array',
         array('%field' => $field, '%table' => $table, '%array' => print_r($values, 1)),

+ 18 - 5
tripal_chado/includes/TripalFields/chado_linker__prop/chado_linker__prop_widget.inc

@@ -40,7 +40,7 @@ class chado_linker__prop_widget extends ChadoFieldWidget {
     if (count($items) > 0) {
       // Check for element values that correspond to fields in the Chado table.
       $fk_value = tripal_get_field_item_keyval($items, 0, 'chado-' . $chado_table . '__' . $lfkey_field, $fk_value);
-      $type_id = tripal_get_field_item_keyval($items, 0, 'chado-' . $chado_table . '__type_id', $type_id);      
+      $type_id = tripal_get_field_item_keyval($items, 0, 'chado-' . $chado_table . '__type_id', $type_id);
       if (array_key_exists($delta, $items)) {
         $record_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $chado_table . '__' . $pkey, $record_id);
         $value = tripal_get_field_item_keyval($items, $delta, 'chado-' . $chado_table . '__value', $value);
@@ -59,9 +59,22 @@ class chado_linker__prop_widget extends ChadoFieldWidget {
 
     // Use default value for the field if it's not already set
     if (!$value && isset($instance['default_value'][$delta])) {
-      $value = $instance['default_value'][$delta]['chado-' . $chado_table . '__value'];
+      $value = $instance['default_value'][$delta]['value'];
     }
-    
+    if (!$type_id) {
+      $vocabulary = $this->instance['settings']['term_vocabulary'];
+      $accession = $this->instance['settings']['term_accession'];
+      $cvterm = tripal_get_cvterm(array(
+        'dbxref_id' => array(
+          'db_id' => array(
+            'name' => $vocabulary,
+          ),
+          'accession' => $accession,
+        ),
+      ));
+      $type_id = $cvterm->cvterm_id;
+    }
+
     $widget['value'] = array(
       '#type' => 'value',
       '#value' => array_key_exists($delta, $items) ? $items[$delta]['value'] : '',
@@ -78,7 +91,7 @@ class chado_linker__prop_widget extends ChadoFieldWidget {
     $widget['chado-' . $chado_table . '__value'] = array(
       '#type' => 'textarea',
       '#default_value' => $value,
-      '#title' => $instance['label'] . ' value',
+      '#title' => $instance['label'],
       '#description' => $instance['description'],
     );
     $widget['chado-' . $chado_table . '__type_id'] = array(
@@ -106,7 +119,7 @@ class chado_linker__prop_widget extends ChadoFieldWidget {
 
     $value = $form_state['values'][$field_name]['und'][$delta]['chado-' . $chado_table . '__value'];
     $form_state['values'][$field_name]['und'][$delta]['value'] = $value;
-    
+
     // If the user removed the property then we want to clear out the other
     // fields so there is no insert.
     if (!$value) {

+ 11 - 10
tripal_chado/includes/TripalFields/local__source_data/local__source_data_formatter.inc

@@ -15,16 +15,17 @@ class local__source_data_formatter extends ChadoFieldFormatter {
 
     $content = 'The data source is not provided.';
     if ($items[0]['value']) {
-      $content = "
-        <dl class=\"tripal-dl\">
-           <dt>Source Name</dt>
-           <dd>: " . $items[0]['value']['schema:name'] . " </dd>
-           <dt>Source Version</dt>
-           <dd>: " . $items[0]['value']['IAO:0000129'] . " </dd>
-           <dt>Source URI</dt>
-           <dd>: " . l($items[0]['value']['data:1047'], $items[0]['value']['data:1047'], array('attributes' => array('target' => '_blank'))) . " </dd>
-        </dl>
-      ";
+      $content = "<dl class=\"tripal-dl\">";
+      if (!empty($items[0]['value']['schema:name'])) {
+        $content .= "<dt>Source Name</dt><dd>: " . $items[0]['value']['schema:name'] . " </dd>";
+      }
+      if (!empty($items[0]['value']['IAO:0000129'])) {
+        $content .= "<dt>Source Version</dt><dd>: " . $items[0]['value']['IAO:0000129'] . " </dd>";
+      }
+      if (!empty($items[0]['value']['data:1047'])) {
+        $content .= "<dt>Source URI</dt><dd>: " . l($items[0]['value']['data:1047'], $items[0]['value']['data:1047'], array('attributes' => array('target' => '_blank'))) . " </dd>";
+      }
+      $content .= "</dl>";
     }
     $element[0] = array(
       // We create a render array to produce the desired markup,

+ 3 - 3
tripal_chado/includes/TripalFields/local__source_data/local__source_data_widget.inc

@@ -47,9 +47,8 @@ class local__source_data_widget extends ChadoFieldWidget {
     );
     $widget['source_data'] = array(
       '#type' => 'fieldset',
-      '#title' => 'Source Data',
-      '#description' => t('The following fields help viewers identify the source
-          where data was obtained for this analysis.'),
+      '#title' => $this->instance['label'],
+      '#description' => $this->instance['description'],
       '#weight' => isset($element['#weight']) ? $element['#weight'] : 0,
       '#delta' => $delta,
     );
@@ -58,6 +57,7 @@ class local__source_data_widget extends ChadoFieldWidget {
       '#title' => 'Data Source Name',
       '#description' => 'The name of the source where data was obtained for this analysis.',
       '#default_value' => $sourcename,
+      '#required' => TRUE,
     );
     $widget['source_data']['chado-analysis__sourceversion'] = array(
       '#type' => 'textfield',

+ 2 - 0
tripal_chado/includes/tripal_chado.entity.inc

@@ -15,12 +15,14 @@ function tripal_chado_entity_create(&$entity, $type) {
     if (!property_exists($entity, 'chado_table')) {
       $entity->chado_table =  NULL;
       $entity->chado_column = NULL;
+      $entity->chado_linker = NULL;
 
       // Add in the Chado table information for this entity type.
       $bundle = tripal_load_bundle_entity(array('name' => $entity->bundle));
       if ($bundle->data_table) {
         $entity->chado_table = $bundle->data_table;
         $entity->chado_column = $bundle->type_column;
+        $entity->chado_linker = $bundle->type_linker_table;
       }
     }
     if (!property_exists($entity, 'chado_record')) {

+ 17 - 7
tripal_chado/includes/tripal_chado.field_storage.inc

@@ -37,13 +37,13 @@ function tripal_chado_field_storage_write($entity_type, $entity, $op, $fields) {
   $type_field = $entity->chado_column;
   $record     = $entity->chado_record;
   $record_id  = $entity->chado_record_id;
+  $linker     = $entity->chado_linker;
   $base_schema = chado_get_schema($base_table);
   $base_pkey = $base_schema['primary key'][0];
 
   // Convert the fields into a key/value list of fields and their values.
   $field_vals = tripal_chado_field_storage_write_merge_fields($fields, $entity_type, $entity);
 
-
   // First, write the record for the base table.  If we have a record id then
   // this is an update and we need to set the primary key.  If not, then this
   // is an insert and we need to set the type_id if the table supports it.
@@ -51,10 +51,10 @@ function tripal_chado_field_storage_write($entity_type, $entity, $op, $fields) {
   if ($record_id) {
     $values[$base_pkey] = $record_id;
   }
-  elseif ($type_field) {
+  elseif ($type_field and !$linker) {
     $values[$type_field] = $cvterm->cvterm_id;
   }
-  $base_record_id = tripal_chado_field_storage_write_table($base_table, $values);
+  $base_record_id = tripal_chado_field_storage_write_table($base_table, $values, $base_table);
 
   // If this is an insert then add the chado_entity record.
   if ($op == FIELD_STORAGE_INSERT) {
@@ -77,7 +77,7 @@ function tripal_chado_field_storage_write($entity_type, $entity, $op, $fields) {
       continue;
     }
     foreach ($details as $delta => $values) {
-      $record_id = tripal_chado_field_storage_write_table($table_name, $values);
+      $record_id = tripal_chado_field_storage_write_table($table_name, $values, $base_table, $base_pkey, $base_record_id);
     }
   }
 }
@@ -106,12 +106,11 @@ function tripal_chado_field_storage_write($entity_type, $entity, $op, $fields) {
  * @return
  *   The unique record ID.
  */
-function tripal_chado_field_storage_write_table($table_name, $values) {
+function tripal_chado_field_storage_write_table($table_name, $values, $base_table, $base_pkey = NULL, $base_record_id= NULL) {
   $schema = chado_get_schema($table_name);
   $fkeys = $schema['foreign keys'];
   $pkey = $schema['primary key'][0];
 
-
   // Fields with a cardinality greater than 1 will often submit an
   // empty form.  We want to remove these empty submissions.  We can detect
   // them if all of the fields are empty.
@@ -154,13 +153,24 @@ function tripal_chado_field_storage_write_table($table_name, $values) {
       return $record[0]->$pkey;
     }
 
+    // If this table_name is a linker table then we want to be sure to add in
+    // the value for the $base_record_id it it isn't set.  This would only
+    // occur on an insert.
+    if (array_key_exists($base_table, $fkeys) and $base_table != $table_name) {
+      foreach ($fkeys[$base_table]['columns'] as $lkey => $rkey) {
+        if ($rkey == $base_pkey and !array_key_exists($lkey, $values) or empty($values[$lkey])) {
+          $values[$lkey] = $base_record_id;
+        }
+      }
+    }
+
     // Insert the values array as a new record in the table but remove the
     // pkey as it should be set.
     $new_vals = $values;
     unset($new_vals[$pkey]);
     $record = chado_insert_record($table_name, $new_vals);
     if ($record === FALSE) {
-      throw new Exception('Could not insert Chado record into table: "' . $table_name . '".');
+      throw new Exception('Could not insert Chado record into table: "' . $table_name . '": ' . print_r($new_vals, TRUE));
     }
     return $record[$pkey];
   }

+ 46 - 39
tripal_chado/includes/tripal_chado.fields.inc

@@ -309,17 +309,17 @@ function tripal_chado_bundle_fields_info_custom(&$info, $details, $entity_type,
       ),
     );
 
-    $field_name = 'so__cds';
-    $field_type = 'so__cds';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => 1,
-      'locked' => FALSE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
+//     $field_name = 'so__cds';
+//     $field_type = 'so__cds';
+//     $info[$field_name] = array(
+//       'field_name' => $field_name,
+//       'type' => $field_type,
+//       'cardinality' => 1,
+//       'locked' => FALSE,
+//       'storage' => array(
+//         'type' => 'field_chado_storage',
+//       ),
+//     );
   }
 
   // GENE TRANSCRIPTS
@@ -836,6 +836,9 @@ function tripal_chado_bundle_instances_info_base(&$info, $entity_type, $bundle,
     //
     // ANALYSIS TABLE
     //
+    elseif ($table_name == 'analysis' and $column_name == 'name') {
+      $base_info['required'] = TRUE;
+    }
     elseif ($table_name == 'analysis' and $column_name == 'program') {
       $base_info['description'] = 'The program name (e.g. blastx, blastp, sim4, genscan. If the analysis was not derived from a software package then provide a very brief description of the pipeline, workflow or method.';
       $base_info['label'] = 'Program, Pipeline, Workflow or Method Name';
@@ -848,6 +851,10 @@ function tripal_chado_bundle_instances_info_base(&$info, $entity_type, $bundle,
       $base_info['label'] = 'Program Version';
       $base_info['description'] = 'The version of the program used to perform this analysis. (e.g. TBLASTX 2.0MP-WashU [09-Nov-2000]. Enter "n/a" if no version is available or applicable.';
     }
+    elseif ($table_name == 'analysis' and $column_name == 'timeexecuted') {
+      $base_info['label'] = 'Date Performed';
+      $base_info['description'] = 'The date and time when the analysis was performed.';
+    }
 
     if ($table_name == 'analysis' and ($column_name == 'sourceuri' or
         $column_name == 'sourceversion' or $column_name == 'sourcename')) {
@@ -1162,34 +1169,34 @@ function tripal_chado_bundle_instances_info_custom(&$info, $entity_type, $bundle
       ),
     );
 
-    $field_name = 'so__cds';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Coding Sequence',
-      'description' => 'Coding sequences.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => 'featureloc',
-        'chado_column' => '',
-        'base_table' => 'featureloc',
-      ),
-      'widget' => array(
-        'type' => 'so__cds_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'so__cds_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
+//     $field_name = 'so__cds';
+//     $info[$field_name] = array(
+//       'field_name' => $field_name,
+//       'entity_type' => $entity_type,
+//       'bundle' => $bundle->name,
+//       'label' => 'Coding Sequence',
+//       'description' => 'Coding sequences.',
+//       'required' => FALSE,
+//       'settings' => array(
+//         'auto_attach' => FALSE,
+//         'chado_table' => 'featureloc',
+//         'chado_column' => '',
+//         'base_table' => 'featureloc',
+//       ),
+//       'widget' => array(
+//         'type' => 'so__cds_widget',
+//         'settings' => array(
+//           'display_label' => 1,
+//         ),
+//       ),
+//       'display' => array(
+//         'default' => array(
+//           'label' => 'above',
+//           'type' => 'so__cds_formatter',
+//           'settings' => array(),
+//         ),
+//       ),
+//     );
   }
 
 

+ 114 - 7
tripal_chado/includes/tripal_chado.semweb.inc

@@ -19,9 +19,11 @@ function tripal_chado_populate_chado_semweb_table() {
   // inserted.
 
   // Now set defaults!
+  tripal_chado_populate_vocab_DC();
   tripal_chado_populate_vocab_EDAM();
   tripal_chado_populate_vocab_ERO();
   tripal_chado_populate_vocab_FOAF();
+  tripal_chado_populate_vocab_HYDRA();
   tripal_chado_populate_vocab_IAO();
   tripal_chado_populate_vocab_LOCAL();
   tripal_chado_populate_vocab_NCBITAXON();
@@ -31,6 +33,7 @@ function tripal_chado_populate_chado_semweb_table() {
   tripal_chado_populate_vocab_SBO();
   tripal_chado_populate_vocab_SCHEMA();
   tripal_chado_populate_vocab_SIO();
+  tripal_chado_populate_vocab_SO();
   tripal_chado_populate_vocab_SWO();
   tripal_chado_populate_vocab_TAXRANK();
   tripal_chado_populate_vocab_TCONTACT();
@@ -55,7 +58,59 @@ function tripal_chado_populate_vocab_FOAF() {
     'Friend of a Friend. A dictionary of people-related terms that can be used in structured data).'
   );
 }
+/**
+ * Adds the Hydra vocabulary
+ */
+function tripal_chado_populate_vocab_HYDRA() {
+
+  tripal_insert_db(array(
+    'name' => 'hydra',
+    'description' => 'A Vocabulary for Hypermedia-Driven Web APIs',
+    'url' => 'https://www.hydra-cg.com/spec/latest/core/',
+    'urlprefix' => 'https://www.hydra-cg.com/spec/latest/core/#{db}:{accession}',
+  ));
+  tripal_insert_cv(
+    'hydra',
+    'A Vocabulary for Hypermedia-Driven Web APIs.'
+  );
 
+  $name = tripal_insert_cvterm(array(
+    'id' => 'hydra:Collection',
+    'name' => 'Collection',
+    'cv_name' => 'hydra',
+    'definition' => 'A collection holding references to a number of related resources.',
+  ));
+  $name = tripal_insert_cvterm(array(
+    'id' => 'hydra:member',
+    'name' => 'member',
+    'cv_name' => 'hydra',
+    'definition' => 'A member of the collection',
+  ));
+  $name = tripal_insert_cvterm(array(
+    'id' => 'hydra:description',
+    'name' => 'description',
+    'cv_name' => 'hydra',
+    'definition' => 'A description.',
+  ));
+  $name = tripal_insert_cvterm(array(
+    'id' => 'hydra:totalItems',
+    'name' => 'totalItems',
+    'cv_name' => 'hydra',
+    'definition' => 'The total number of items referenced by a collection.',
+  ));
+  $name = tripal_insert_cvterm(array(
+    'id' => 'hydra:title',
+    'name' => 'title',
+    'cv_name' => 'hydra',
+    'definition' => 'A title, often used along with a description.',
+  ));
+  $name = tripal_insert_cvterm(array(
+    'id' => 'hydra:PartialCollectionView',
+    'name' => 'PartialCollectionView',
+    'cv_name' => 'hydra',
+    'definition' => 'A PartialCollectionView describes a partial view of a Collection. Multiple PartialCollectionViews can be connected with the the next/previous properties to allow a client to retrieve all members of the collection.',
+  ));
+}
 /**
  * Adds the RDFS database and terms.
  */
@@ -64,7 +119,7 @@ function tripal_chado_populate_vocab_RDFS() {
     'name' => 'rdfs',
     'description' => 'Resource Description Framework Schema',
     'url' => 'https://www.w3.org/TR/rdf-schema/',
-    'urlprefix' => 'https://www.w3.org/TR/rdf-schema/#ch_',
+    'urlprefix' => 'https://www.w3.org/TR/rdf-schema/#ch_{accession}',
   ));
   tripal_insert_cv(
     'rdfs',
@@ -99,7 +154,6 @@ function tripal_chado_populate_vocab_SCHEMA() {
     'Schema.org. Schema.org is sponsored by Google, Microsoft, Yahoo and Yandex. The vocabularies are developed by an open community process.'
   );
 
-
   $term = tripal_insert_cvterm(array(
     'id' => 'schema:name',
     'name' => 'name',
@@ -172,6 +226,13 @@ function tripal_chado_populate_vocab_SCHEMA() {
       tripal_associate_chado_semweb_term($table, 'type_id', $term);
     }
   }
+
+  $term = tripal_insert_cvterm(array(
+    'id' => 'schema:ItemPage',
+    'name' => 'ItemPage',
+    'cv_name' => 'schema',
+    'definition' => 'A page devoted to a single item, such as a particular product or hotel.',
+  ));
 }
 
 /**
@@ -217,6 +278,41 @@ function tripal_chado_populate_vocab_SIO() {
   ));
   tripal_associate_chado_semweb_term('cvterm', 'cv_id', $term);
 }
+
+/**
+ * Adds the details for the SO vocab and db.
+ */
+function tripal_chado_populate_vocab_SO() {
+  tripal_insert_db(array(
+    'name' => 'SO',
+    'description' => 'The sequence ontology.',
+    'url' => 'http://www.sequenceontology.org/',
+    'urlprefix' => 'http://www.sequenceontology.org/browser/current_svn/term/{db}:{accession}',
+  ));
+  tripal_insert_cv('sequence', 'The sequence ontology.');
+}
+
+/**
+ * Adds the DC database.
+ */
+function tripal_chado_populate_vocab_DC() {
+  tripal_insert_db(array(
+    'name' => 'dc',
+    'description' => 'DCMI Metadata Terms.',
+    'url' => 'http://purl.org/dc/dcmitype/',
+    'urlprefix' => 'http://purl.org/dc/terms/{accession}',
+  ));
+  tripal_insert_cv(
+    'dc',
+    'DCMI Metadata Terms.'
+  );
+  $term = tripal_insert_cvterm(array(
+    'id' => 'dc:Service',
+    'name' => 'Service',
+    'cv_name' => 'dc',
+    'definition' => 'A system that provides one or more functions.',
+  ));
+}
 /**
  * Adds the EDAM database and terms.
  */
@@ -518,16 +614,19 @@ function tripal_chado_populate_vocab_IAO() {
  * ontology.
  */
 function tripal_chado_populate_vocab_LOCAL() {
+  global $base_path;
 
   tripal_insert_db(array(
     'name' => 'null',
-    'description' => 'No online database.'
+    'description' => 'No online database.',
+    'url' => $base_path . 'cv/lookup/null',
+    'urlprefix' => $base_path. 'cv/lookup/{db}/{accession}',
   ));
   tripal_insert_db(array(
     'name' => 'local',
     'description' => 'Terms created for this site.',
-    'url' => '/cv/lookup',
-    'urlprefix' => '/cv/lookup/{db}/{accession}',
+    'url' => $base_path . 'cv/lookup/local',
+    'urlprefix' => $base_path . 'cv/lookup/{db}/{accession}',
   ));
 
 
@@ -786,7 +885,7 @@ function tripal_chado_populate_vocab_LOCAL() {
   // TODO: these probably have real terms we can use.
 
   $term = tripal_insert_cvterm(array(
-    'id' => 'locak:rank',
+    'id' => 'local:rank',
     'name' => 'rank',
     'definition' => 'A taxonmic rank',
     'cv_name' => 'local',
@@ -1345,7 +1444,7 @@ function tripal_chado_populate_vocab_UO() {
     'url' => 'http://purl.obolibrary.org/obo/uo',
     'urlprefix' => 'http://purl.obolibrary.org/obo/TAXRANK_',
   ));
-  tripal_insert_cv('uo','Units of Measurement Ontology');
+  tripal_insert_cv('uo', 'Units of Measurement Ontology');
 
   $term = tripal_insert_cvterm(array(
     'id' => 'UO:0000000',
@@ -1361,6 +1460,14 @@ function tripal_chado_populate_vocab_UO() {
  */
 function tripal_chado_populate_vocab_TAXRANK() {
 
+  tripal_insert_db(array(
+    'name' => 'TAXRANK',
+    'description' => 'A vocabulary of taxonomic ranks (species, family, phylum, etc)',
+    'url' => 'http://www.obofoundry.org/ontology/taxrank.html',
+    'urlprefix' => 'http://purl.obolibrary.org/obo/{db}_{accession}',
+  ));
+  tripal_insert_cv('taxonomic_rank', 'A vocabulary of taxonomic ranks (species, family, phylum, etc)');
+
   $term = tripal_get_cvterm(array('id' => 'TAXRANK:0000005'));
   tripal_associate_chado_semweb_term('organism', 'genus', $term);
 

+ 27 - 0
tripal_chado/includes/tripal_chado.vocab_storage.inc

@@ -15,6 +15,33 @@ function tripal_chado_vocab_storage_info() {
     ),
   );
 }
+/**
+ * Implements hook_vocab_get_vocabulary().
+ *
+ * This hook is created by the Tripal module and is not a Drupal hook.
+ */
+function tripal_chado_vocab_get_vocabulary($vocabulary) {
+  // It's possible that Chado is not available (i.e. it gets renamed
+  // for copying) but Tripal has already been prepared and the
+  // entities exist.  If this is the case we don't want to run the
+  // commands below.
+  if (!chado_table_exists('cv')) {
+    return FALSE;
+  }
+  $sql = "
+     SELECT DB.name as name, CV.name as short_name, DB.description, DB.url
+     FROM {db} DB
+      INNER JOIN {dbxref} DBX on DBX.db_id = DB.db_id
+      INNER JOIN {cvterm} CVT on CVT.dbxref_id = DBX.dbxref_id
+      INNER JOIN {cv} CV on CV.cv_id = CVT.cv_id
+     WHERE
+      DB.name = :name
+     LIMIT 1 OFFSET 0
+  ";
+  $result = chado_query($sql, array(':name' => $vocabulary));
+  $result = $result->fetchAssoc();
+  return $result;
+}
 
 /**
  * Implements hook_vocab_get_term().

+ 122 - 0
tripal_chado/tripal_chado.install

@@ -873,4 +873,126 @@ function tripal_chado_update_7304() {
     $error = $e->getMessage();
     throw new DrupalUpdateException('Could not perform update: '. $error);
   }
+}
+/**
+ * Adding missing cv/db details and cvterms.
+ */
+function tripal_chado_update_7305() {
+  try {
+    tripal_insert_db(array(
+      'name' => 'rdfs',
+      'description' => 'Resource Description Framework Schema',
+      'url' => 'https://www.w3.org/TR/rdf-schema/',
+      'urlprefix' => 'https://www.w3.org/TR/rdf-schema/#ch_{accession}',
+    ));
+    tripal_insert_cv('rdfs', 'Resource Description Framework Schema');
+    tripal_insert_db(array(
+      'name' => 'SO',
+      'description' => 'The sequence ontology.',
+      'url' => 'http://www.sequenceontology.org/',
+      'urlprefix' => 'http://www.sequenceontology.org/browser/current_svn/term/{db}:{accession}',
+    ));
+    tripal_insert_cv('sequence', 'The sequence ontology.');
+    tripal_insert_db(array(
+      'name' => 'TAXRANK',
+      'description' => 'A vocabulary of taxonomic ranks (species, family, phylum, etc)',
+      'url' => 'http://www.obofoundry.org/ontology/taxrank.html',
+      'urlprefix' => 'http://purl.obolibrary.org/obo/{db}_{accession}',
+    ));
+    tripal_insert_cv('taxonomic_rank', 'A vocabulary of taxonomic ranks (species, family, phylum, etc)');
+    tripal_insert_db(array(
+      'name' => 'hydra',
+      'description' => 'A Vocabulary for Hypermedia-Driven Web APIs',
+      'url' => 'https://www.hydra-cg.com/spec/latest/core/',
+      'urlprefix' => 'https://www.hydra-cg.com/spec/latest/core/#{db}:{accession}',
+    ));
+    tripal_insert_cv(
+      'hydra',
+      'A Vocabulary for Hypermedia-Driven Web APIs.'
+    );
+    tripal_insert_db(array(
+      'name' => 'dc',
+      'description' => 'DCMI Metadata Terms.',
+      'url' => 'http://purl.org/dc/dcmitype/',
+      'urlprefix' => 'http://purl.org/dc/terms/{accession}',
+    ));
+    tripal_insert_cv(
+      'dc',
+      'DCMI Metadata Terms.'
+    );
+    $term = tripal_insert_cvterm(array(
+      'id' => 'dc:Service',
+      'name' => 'Service',
+      'cv_name' => 'dc',
+      'definition' => 'A system that provides one or more functions.',
+    ));
+    $name = tripal_insert_cvterm(array(
+      'id' => 'hydra:Collection',
+      'name' => 'Collection',
+      'cv_name' => 'hydra',
+      'definition' => 'A collection holding references to a number of related resources.',
+    ));
+    $name = tripal_insert_cvterm(array(
+      'id' => 'hydra:member',
+      'name' => 'member',
+      'cv_name' => 'hydra',
+      'definition' => 'A member of the collection',
+    ));
+    $name = tripal_insert_cvterm(array(
+      'id' => 'hydra:description',
+      'name' => 'description',
+      'cv_name' => 'hydra',
+      'definition' => 'A description.',
+    ));
+    $name = tripal_insert_cvterm(array(
+      'id' => 'hydra:totalItems',
+      'name' => 'totalItems',
+      'cv_name' => 'hydra',
+      'definition' => 'The total number of items referenced by a collection.',
+    ));
+    $name = tripal_insert_cvterm(array(
+      'id' => 'hydra:title',
+      'name' => 'title',
+      'cv_name' => 'hydra',
+      'definition' => 'A title, often used along with a description.',
+    ));
+    $name = tripal_insert_cvterm(array(
+      'id' => 'hydra:PartialCollectionView',
+      'name' => 'PartialCollectionView',
+      'cv_name' => 'hydra',
+      'definition' => 'A PartialCollectionView describes a partial view of a Collection. Multiple PartialCollectionViews can be connected with the the next/previous properties to allow a client to retrieve all members of the collection.',
+    ));
+    $term = tripal_insert_cvterm(array(
+      'id' => 'schema:ItemPage',
+      'name' => 'ItemPage',
+      'cv_name' => 'schema',
+      'definition' => 'A page devoted to a single item, such as a particular product or hotel.',
+    ));
+    global $base_path;
+
+    tripal_insert_db(array(
+      'name' => 'null',
+      'description' => 'No online database.',
+      'url' => $base_path . 'cv/lookup/null',
+      'urlprefix' => $base_path. 'cv/lookup/{db}/{accession}',
+    ));
+    tripal_insert_db(array(
+      'name' => 'local',
+      'description' => 'Terms created for this site.',
+      'url' => $base_path . 'cv/lookup/local',
+      'urlprefix' => $base_path . 'cv/lookup/{db}/{accession}',
+    ));
+    $term = tripal_insert_cvterm(array(
+      'id' => 'local:rank',
+      'name' => 'rank',
+      'definition' => 'A taxonmic rank',
+      'cv_name' => 'local',
+    ));
+
+  }
+  catch (\PDOException $e) {
+    $transaction->rollback();
+    $error = $e->getMessage();
+    throw new DrupalUpdateException('Could not perform update: '. $error);
+  }
 }

+ 4 - 0
tripal_daemon/.gitattributes

@@ -0,0 +1,4 @@
+*.inc linguist-language=PHP
+*.info linguist-language=PHP
+*.install linguist-language=PHP
+*.module linguist-language=PHP

+ 44 - 0
tripal_daemon/README.md

@@ -0,0 +1,44 @@
+# Tripal Daemon
+
+This module is meant to provide a simple means of creating a robust
+command-line-driven, fully bootstrapped PHP Daemon. It uses the PHP-Daemon
+(https://github.com/shaneharter/PHP-Daemon) Library to create the Daemon (via
+the Libraries API) in order to not re-invent the wheel ;-).
+
+
+## FEATURES
+* Provides a Drush interface to start/stop your Daemon.
+* Your daemon starts in the background and is detached from the current
+   terminal.
+* Daemon will run all Tripal Jobs submitted within 20 seconds.
+* A log including the number of jobs executed, their identifiers and results.
+* Lock Files, Automatic restart (8hrs default) and Built-in Signal Handling &
+   Event Logging are only a few of the features provided by the Daemon API
+   making this a fully featured & robust Daemon.
+
+## REQUIREMENTS
+* Libraries API (https://www.drupal.org/project/libraries)
+* PHP-Daemon Library version 2.0 (https://github.com/shaneharter/PHP-Daemon)
+* Drush 5.x (https://github.com/drush-ops/drush)
+* Drush Daemon API (https://www.drupal.org/project/drushd)
+
+## INSTALLATION
+* Install all required modules as per their instructions.
+* Install this module as you would normally install a contributed drupal
+   module. See:https://drupal.org/documentation/install/modules-themes/modules-7
+   for further information.
+* Download the PHP-Daemon Library and extract it in your sites/all/libraries
+   directory. The folder must be named "PHP-Daemon".
+
+## TRIPAL DAEMON USAGE
+* Start Daemon
+    drush trpjob-daemon start
+* Stop Daemon
+    drush trpjob-daemon stop
+* Check the Status
+    drush trpjob-daemon status
+* Show the Log
+   * List the last 10 lines of the log file:
+      drush trpjob-daemon show-log
+   * List the last N lines of the log file:
+      drush trpjob-daemon show-log --num_lines=N

+ 64 - 0
tripal_daemon/README.txt

@@ -0,0 +1,64 @@
+
+CONTENTS OF THIS FILE
+---------------------
+ * Introduction
+ * Features
+ * Requirements
+ * Installation
+ * Configuration
+ * Daemon Usage
+
+INTRODUCTION
+------------
+This module is meant to provide a simple means of creating a robust
+command-line-driven, fully bootstrapped PHP Daemon. It uses the PHP-Daemon
+(https://github.com/shaneharter/PHP-Daemon) Library to create the Daemon (via
+the Libraries API) in order to not re-invent the wheel ;-).
+
+
+FEATURES
+--------
+ * Provides a Drush interface to start/stop your Daemon.
+ * Your daemon starts in the background and is detached from the current
+   terminal.
+ * Daemon will run all Tripal Jobs submitted within 20 seconds.
+ * A log including the number of jobs executed, their identifiers and results.
+ * Lock Files, Automatic restart (8hrs default) and Built-in Signal Handling &
+   Event Logging are only a few of the features provided by the Daemon API
+   making this a fully featured & robust Daemon.
+
+REQUIREMENTS
+------------
+ * Libraries API (https://www.drupal.org/project/libraries)
+ * PHP-Daemon Library version 2.0 (https://github.com/shaneharter/PHP-Daemon)
+ * Drush 5.x (https://github.com/drush-ops/drush)
+ * Drush Daemon API (https://www.drupal.org/project/drushd)
+
+INSTALLATION
+------------
+ * Install all required modules as per their instructions.
+ * Install this module as you would normally install a contributed drupal
+   module. See:https://drupal.org/documentation/install/modules-themes/modules-7
+   for further information.
+ * Download the PHP-Daemon Library and extract it in your sites/all/libraries
+   directory. The folder must be named "PHP-Daemon".
+
+CONFIGURATION
+-------------
+The module has no menu or modifiable settings.  There is no configuration.  When
+enabled, the module will provide a number of drush commands for control of the
+Tripal Daemon from the command-line.
+
+TRIPAL DAEMON USAGE
+-------------------
+* Start Daemon
+    drush trpjob-daemon start
+* Stop Daemon
+    drush trpjob-daemon stop
+* Check the Status
+    drush trpjob-daemon status
+* Show the Log
+   * List the last 10 lines of the log file:
+      drush trpjob-daemon show-log
+   * List the last N lines of the log file:
+      drush trpjob-daemon show-log --num_lines=N

+ 115 - 0
tripal_daemon/TripalDaemon.inc

@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @file
+ * Implements the Tripal Daemon functionality by using the Daemon API.
+ */
+
+/**
+ * This is the main class for the Tripal Daemon.
+ *
+ * It extends the DaemonAPIDaemon class provided by the Daemon API in order
+ * to implement tripal job checking and execution functionality.
+ */
+class TripalDaemon extends DrushDaemon {
+
+  // OPTIONAL: Set how often in seconds your executeTask() should be called.
+  // Keep in mind that this time does not include the amount of time spent
+  // executing your tasks. For example, if you set this to 5 seconds and you
+  // have 2 tasks in your execute_tasks() function, each of which take 15
+  // seconds, then your loop will iterate (and thus your execute_task()
+  // function will be called again) before your tasks finish.
+  // CODING STANDARDS: Can't change this variable to lowerCamel since it
+  // inherits from a library class.
+  protected $loop_interval = 20;
+
+  /**
+   * Implements DaemonAPIDaemon::executeTask() function.
+   *
+   * This gets executed once per loop iteration & does the following:
+   *   1. Checks to see if there are any Tripal Jobs waiting to be executed.
+   *   2. If there are then they are run (jobs with a higher priority and higher
+   *      job_id are run first.
+   *
+   * This function will log how many jobs have been found and when each one was
+   * started/completed, as well as, it's status upon completion.
+   *
+   * @param int $iteration_number
+   *   This is an integer stating the current iteration of the loop you are on.
+   */
+  protected function executeTask($iteration_number) {
+
+    // When sorting the job list we want to use version specific SQL and thus
+    // need to know the postgreSQL version to determine what SQL to execute.
+    $version_string = db_query('SELECT version()')->fetchField();
+    if (preg_match('/PostgreSQL (\d+)\.(\d+)/', $version_string, $matches)) {
+      $version = array('major' => $matches[1], 'minor' => $matches[2]);
+    }
+    // If we can't determine the version then use the deprecated method.
+    else {
+      $version = array('major' => 8, 'minor' => 4);
+    }
+
+    // First check to see if there are any tripal jobs to be run.
+    if ($version['major'] >= 9 ) {
+      $waiting_jobs = db_query(
+        "SELECT
+          count(*) as count,
+          array_to_string(array_agg(j.job_id ORDER BY j.priority ASC, j.job_id ASC),'|') as jobs
+        FROM {tripal_jobs} j
+        WHERE j.status = 'Waiting'"
+      )->fetchObject();
+    }
+    else {
+     $waiting_jobs = db_query(
+        "SELECT
+          count(*) as count,
+          array_to_string(array_agg(j.job_id),'|') as jobs
+        FROM (SELECT * FROM {tripal_jobs} WHERE j.status = 'Waiting' ORDER BY priority ASC, job_id ASC) as j"
+      )->fetchObject();
+    }
+
+    $num_waiting_jobs = $waiting_jobs->count;
+    $job_ids = explode('|', $waiting_jobs->jobs);
+
+    // If there are then run them and log the output.
+    if ($num_waiting_jobs > 0) {
+      $this->log($num_waiting_jobs . ' Waiting Tripal Jobs... '
+        . 'Running waiting job(s) now.');
+
+      // Launch all tripal jobs :) Yay for bootstrapping!!
+      foreach ($job_ids as $id) {
+        $this->log('Starting Job (ID=' . $id . ')', '', 1);
+
+        // We would like to log the output from the job.
+        // However, most tripal jobs simply print to the screen :-(
+        // Thus we have to use output buffering to capture the output.
+        // Start Buffering.
+        ob_start();
+
+        // Launch Tripal Job.
+        tripal_launch_job(FALSE, $id);
+
+        // Save the buffer to the log and stop buffering.
+        $this->log(str_repeat('=', 80));
+        $this->log(ob_get_clean());
+        $this->log(str_repeat('=', 80));
+
+        // Report job details.
+        $job = db_query(
+          "SELECT j.*
+          FROM {tripal_jobs} j
+          WHERE j.job_id = :jid",
+          array(':jid' => $id)
+        )->fetchObject();
+        $this->log("Job completed at "
+        . date('d M Y H:i:s', $job->end_time) . " with a status of '"
+        . $job->status . "'", "", 1);
+      }
+    }
+    else {
+      $this->log('There are no Tripal Jobs to run');
+    }
+
+  }
+}

+ 82 - 0
tripal_daemon/includes/tripal_daemon.blocks.inc

@@ -0,0 +1,82 @@
+<?php
+/** 
+ * @file
+ * Contains functions related to administrative blocks for daemon monitoring.
+ */
+
+/**
+ * Implements hook_block_info().
+ */
+function tripal_daemon_block_info() {
+  $blocks = array();
+
+  $blocks['tripal_daemon_status'] = array(
+    'info' => t('Tripal Daemon Status'),
+  );
+  
+  return $blocks;
+}
+
+/**
+ * Implements hook_block_view().
+ */
+function tripal_daemon_block_view($delta='') {
+  $block = array();
+
+  switch($delta) {
+    case 'tripal_daemon_status':
+      $is_running = drushd_is_daemon_running('tripal_daemon');
+      $status_class = ($is_running) ? 'active' : 'inactive';
+      $block['subject'] = t('Job Daemon Status');
+      $block['content'] = theme_tripal_daemon_status_block_content();
+      break;
+  }
+  
+  return $block;
+}
+
+/** 
+ *
+ */
+function theme_tripal_daemon_status_block_content() {
+  $output = '';
+
+  // Get information.
+  $is_running = drushd_is_daemon_running('tripal_daemon');
+  $status_file = drushd_get_daemon_status_file('tripal_daemon');
+  $status = unserialize(file_get_contents($status_file));
+
+  $status_class = ($is_running) ? 'active' : 'inactive';
+
+  // Theme content.
+  drupal_add_css(drupal_get_path('module','tripal_daemon') . '/theme/status_block.css');
+
+  $output .= '<div class="daemon-status">';
+  if ($is_running) {
+    $output .= theme_image(array(
+      'path' => 'misc/message-24-ok.png',
+      'alt' => 'status-ok',
+    ));
+    $output .= 'Running';
+  }
+  else {
+    $output .= theme_image(array(
+      'path' => 'misc/message-24-error.png',
+      'alt' => 'status-error',
+    ));
+    $output .= 'Stopped';
+  }
+  $output .= '</div>';
+
+  $output .= '<ul>';
+  foreach ($status as $k => $v) {
+    if (is_bool($v)) {
+      $v = ($v) ? 'True' : 'False';
+    }
+
+    $output .= '<li><strong>' . $k . '</strong>: ' . $v . '</li>';
+  }
+  $output .= '</ul>';
+
+  return '<div class="inner '.$status_class.'">' . $output . '</div>';
+}

+ 29 - 0
tripal_daemon/theme/status_block.css

@@ -0,0 +1,29 @@
+/**
+ * Tripal Daemon Status Block.
+ */
+#dashboard #block-tripal-daemon-tripal-daemon-status div.content,
+  div#block-tripal-daemon-tripal-daemon-status div.content {
+    padding: 0;
+}
+#block-tripal-daemon-tripal-daemon-status div.content div.inner {
+  padding: 15px; 
+}
+#block-tripal-daemon-tripal-daemon-status .active {
+  background-color: #f8fff0; 
+  color: #234600;
+}
+#block-tripal-daemon-tripal-daemon-status .inactive {
+  background-color: #fef5f1;
+  color: #8c2e0b;
+}
+
+#block-tripal-daemon-tripal-daemon-status .daemon-status {
+  font-weight: bolder;
+  font-size: 1.2em;
+  margin-bottom: 15px;
+}
+#block-tripal-daemon-tripal-daemon-status .daemon-status img {
+  top: 6px;
+  position: relative;
+  padding-right: 5px;
+}

+ 65 - 0
tripal_daemon/tripal_daemon.drush.inc

@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Implementation of the Tripal Daemon Drush commands.
+ */
+
+/**
+ * Implements hook_drush_help().
+ */
+function tripal_daemon_drush_help($command) {
+  switch ($command) {
+    case 'drush:tripal-jobs-daemon':
+      return dt('Use Tripal Jobs Deamon to manage Tripal Job execution.');
+  }
+}
+
+/**
+ * Implements hook_drush_command().
+ */
+function tripal_daemon_drush_command() {
+  $items = array();
+  $items['tripal-jobs-daemon'] = array(
+    'description' => dt('Use Tripal Jobs Deamon to manage Tripal Job execution.'),
+    'arguments' => array(
+      'start'    => 'Start the daemon.',
+      'status'   => 'Display status information about the daemon.',
+      'stop'     => 'Stop the daemon.',
+      'show-log' => 'Show the log file.',
+    ),
+    'options' => array(
+      'num_lines' => 'The number of lines of the log file to show.',
+      'child' => array(
+        'hidden' => TRUE,
+        'description' => 'This option should only be passed via '
+        . 'drush_invoke_process and essentially just allows my command '
+        . 'to not fork bomb',
+      ),
+    ),
+    'examples' => array(
+      'drush trpjob-daemon start' => 'Start the daemon.',
+      'drush trpjob-daemon status' => 'Show the current status of the daemon.',
+      'drush trpjob-daemon stop'              => 'Stop the daemon.',
+      'drush trpjob-daemon show-log' => 'Show the last 10 lines of the log file.',
+      'drush trpjob-daemon show-log --num_lines=50' => 'Show the last 10 lines of the log file.',
+    ),
+    'aliases' => array('trpjob-daemon'),
+  );
+
+  return $items;
+}
+
+/**
+ * Drush Command for Daemonized management of Tripal Jobs.
+ *
+ * Simply plugs into the Daemon API for easier running. This is equivalent to
+ *   drush jobs-daemon $action tripal_daemon.
+ *
+ * @param string $action
+ *   One of 'start','stop','restart',status','show-log'. Meant to indicate what
+ *   you want the daemon to do.
+ */
+function drush_tripal_daemon_tripal_jobs_daemon($action) {
+  drush_drushd_daemon($action, 'tripal_daemon');
+}

+ 9 - 0
tripal_daemon/tripal_daemon.info

@@ -0,0 +1,9 @@
+name = Tripal Jobs Daemon
+description = Creates a Daemon to run Tripal Jobs as they are submitted.
+core = 7.x
+package = Tripal Extensions
+
+dependencies[] = drushd
+dependencies[] = tripal
+
+files[] = TripalDaemon.inc

+ 25 - 0
tripal_daemon/tripal_daemon.module

@@ -0,0 +1,25 @@
+<?php
+/**
+ * @file
+ * Non-Drush Tripal Daemon functionality.
+ */
+
+require_once('includes/tripal_daemon.blocks.inc');
+
+/**
+ * Implements hook_daemon_api_info().
+ *
+ * Registers our Daemon with the Daemon API
+ */
+function tripal_daemon_daemon_api_info() {
+  $daemon = array();
+
+  $daemon['tripal_daemon'] = array(
+    'machine_name' => 'tripal_daemon',
+    'name' => 'Tripal Job Daemon',
+    'module' => 'tripal_daemon',
+    'class' => 'TripalDaemon',
+  );
+
+  return $daemon;
+}

+ 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

+ 230 - 38
tripal_ws/includes/TripalWebService.inc

@@ -2,65 +2,257 @@
 
 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';
+
+
+  // --------------------------------------------------------------------------
+  //              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;
+
+  /**
+   * The URL at which Tripal web services are found.  This is used
+   * for creating the IRI for resources.
+   */
+  protected $base_path;
 
-  private $context;
-  private $response;
-  private $supportedClasses;
+  // --------------------------------------------------------------------------
+  //                             CONSTRUCTORS
+  // --------------------------------------------------------------------------
+  /**
+   * Implements the constructor.
+   */
+  public function __construct($base_path) {
+    if (!$base_path) {
+      throw new Exception('Pleaes provide a $base_path argument when creating a new TripalWebService.');
+    }
+
+    // Create a default resource so that the service always some something.
+    $this->resource = new TripalWebServiceResource($base_path);
+
+    // Intialize the private members variables.
+    $this->path = array();
+    $this->params = array();
+    $this->base_path = $base_path;
+  }
 
+  // --------------------------------------------------------------------------
+  //                          OVERRIDEABLE FUNCTIONS
+  // --------------------------------------------------------------------------
+
+
+  /**
+   * 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.
+  }
+
+  // --------------------------------------------------------------------------
+  //                     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;
+  }
 
   /**
+   * 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 __construct() {
-    $this->context = array();
-    $this->response = array();
-    $this->supportedClasses = array();
-    $this->possibleStates = array();
+  public function getVersion($sanatize = FALSE) {
 
-    // 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#');
+    $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();
   }
 
 
-  public function addSupportedClass($type, $title, $description, $operations = array(), $properties = array()) {
-    // TODO: add some checks.
 
-    $this->supportedClasses[] = array(
+  /**
+   * 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->setIDs($data);
 
-  public function getSupportedClasses() {
-    return $this->supportedClasses;
+    return array_merge($json_ld, $data);
   }
 
-  public function addContextItem($name, $details) {
-    $this->context[$name] = $details;
+  /**
+   * Retreives the service URL for this service.
+   */
+  public function getServicePath() {
+    $class = get_class($this);
+    $version = $this->getVersion();
+    $type = $class::$type;
+    return $this->base_path . '/' . $type . '/' . $version;
   }
 
-  public function getContext() {
-    return $this->context;
+  /**
+   * 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() {
+
+    if ($this->resource) {
+      return $this->resource->getData();
+    }
+    return array();
   }
 
-  public function getDocumentation() {
-     return array(
-       '@context' => $this->getContext(),
-       '@id' => '',
-       '@type' => 'ApiDocumentation',
-       'supportedClass' => $this->getSupportedClasses(),
-       'supportedStatus' => $this->getSuportedStatus(),
-     );
+  /**
+   * 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;
+  }
+
+
+
+  /**
+   * Set an error for the service.
+   *
+   * @param $message
+   *   The error message to report.
+   */
+  public function setError($message) {
+    $this->resource = new TripalWebServiceResource($this->base_path);
+    $this->resource->setID('error');
+    $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';
+}

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

@@ -0,0 +1,648 @@
+<?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($base_path) {
+    parent::__construct($base_path);
+  }
+
+  /**
+   * @see TripalWebService::handleRequest()
+   */
+  public function handleRequest() {
+
+    // Get the content type.
+    $ctype     = (count($this->path) > 0) ? $this->path[0] : '';
+    $entity_id = (count($this->path) > 1) ? $this->path[1] : '';
+    $expfield  = (count($this->path) > 2) ? $this->path[2] : '';
+
+    // If we have a content type then list all of the entities that belong
+    // to it.
+    if ($ctype and !$entity_id and !$expfield) {
+      $this->doContentTypeList($ctype);
+    }
+    // If we have an entity ID then build the resource for a single entity.
+    else if ($ctype and $entity_id and !$expfield) {
+      $this->doEntity($ctype, $entity_id);
+    }
+    else if ($ctype and $entity_id and $expfield) {
+      $this->doExpandedField($ctype, $entity_id, $expfield);
+    }
+    // Otherwise just list all of the available content types.
+    else {
+      $this->doAllTypesList();
+    }
+  }
+
+  /**
+   * Creates a resource for an expanded field of an entity.
+   */
+  private function doExpandedField($ctype, $entity_id, $expfield) {
+    $service_path = $this->getServicePath() . '/' . urlencode($ctype) . '/' . $entity_id;
+    $this->resource = new TripalWebServiceResource($service_path);
+
+    // Get the TripalBundle, TripalTerm and TripalVocab for this type.
+    $bundle = tripal_load_bundle_entity(array('label' => $ctype));
+    $term = entity_load('TripalTerm', array('id' => $bundle->term_id));
+    $term = reset($term);
+    $vocab = $term->vocab;
+
+    // Get the TripalEntity
+    $entity = tripal_load_entity('TripalEntity', array('id' => $entity_id));
+    $entity = reset($entity);
+
+    // If we couldn't match this field argument to a field and entity then return
+    if (!$entity) {
+      throw new Exception("Canno find this entity.");
+    }
+
+    list($field, $instance, $term) = $this->findField($bundle, $expfield);
+
+    // Next add in the ID and Type for this resources.
+    $key = $term['name'];
+    $key_adj = strtolower(preg_replace('/ /', '_', $term['name']));
+    $this->resource->addContextItem($key_adj, $term['url']);
+    $this->resource->setID(urlencode($key));
+    $this->resource->setType($key_adj);
+
+    // Attach the field and then add it's values to the response.
+    field_attach_load($entity->type, array($entity->id => $entity),
+        FIELD_LOAD_CURRENT, array('field_id' => $field['id']));
+
+    $this->addEntityField($key_adj, $term, $entity, $bundle, $field, $instance, $service_path, $expfield);
+  }
+
+  /**
+   * Find the field whose term matches the one provied.
+   */
+  private function findField($bundle, $expfield) {
+
+    $value = array();
+    $instances = field_info_instances('TripalEntity', $bundle->name);
+    foreach ($instances as $instance) {
+      $field_name = $instance['field_name'];
+      $field = field_info_field($field_name);
+      $vocabulary = $instance['settings']['term_vocabulary'];
+      $accession = $instance['settings']['term_accession'];
+      $temp_term = tripal_get_term_details($vocabulary, $accession);
+      if ($temp_term['name'] == $expfield) {
+        return array($field, $instance, $temp_term);
+      }
+    }
+  }
+
+  /**
+   * Creates a resource for a single entity.
+   */
+  private function doEntity($ctype, $entity_id) {
+    $service_path = $this->getServicePath() . '/' . urlencode($ctype);
+    $this->resource = new TripalWebServiceResource($service_path);
+
+    // 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);
+    $vocab = $term->vocab;
+
+    // Add the vocabulary to the context.
+    $this->resource->addContextItem($term->name, $term->url);
+
+    // Get the TripalEntity
+    $entity = tripal_load_entity('TripalEntity', array('id' => $entity_id));
+    $entity = reset($entity);
+
+    $itemPage = tripal_get_term_details('schema', 'ItemPage');
+    $label = tripal_get_term_details('rdfs', 'label');
+    $this->resource->setID($entity_id);
+    $this->resource->setType($term->name);
+    $this->resource->addContextItem('label', $label['url']);
+    $this->resource->addContextItem('ItemPage', $itemPage['url']);
+    $this->resource->addProperty('label', $entity->title);
+    $this->resource->addProperty('ItemPage', url('/bio_data/' . $entity->id, array('absolute' => TRUE)));
+
+    $this->addEntityFields($entity, $bundle, $term, $service_path);
+
+//    tripal_ws_services_v0_1_get_content_add_fields($entity, $bundle, $api_url, $response, $ws_path, $ctype, $entity_id, $params);
+//    tripal_ws_services_v0_1_write_context($response, $ctype);
+  }
+
+  /**
+   * Adds the fields as properties of an entity resource.
+   */
+  private function addEntityFields($entity, $bundle, $term, $service_path) {
+
+    // If the entity is set to hide fields that have no values then we
+    // want to honor that in the web services too.
+    $hide_fields = tripal_get_bundle_variable('hide_empty_field', $bundle->id, 'hide');
+
+    // Get information about the fields attached to this bundle and sort them
+    // in the order they were set for the display.
+    $instances = field_info_instances('TripalEntity', $bundle->name);
+
+    uasort($instances, function($a, $b) {
+      $a_weight = (is_array($a) && isset($a['widget']['weight'])) ? $a['widget']['weight'] : 0;
+      $b_weight = (is_array($b) && isset($b['widget']['weight'])) ? $b['widget']['weight'] : 0;
+
+      if ($a_weight == $b_weight) {
+        return 0;
+      }
+      return ($a_weight < $b_weight) ? -1 : 1;
+    });
+
+    // Iterate through the fields and add each value to the response.
+    //$response['fields'] = $fields;
+    foreach ($instances as $field_name => $instance) {
+
+      // Skip hidden fields.
+      if ($instance['display']['default']['type'] == 'hidden') {
+        continue;
+      }
+
+      // Get the information about this field.
+      $field = field_info_field($field_name);
+
+      // By default, the label for the key in the output should be the
+      // term from the vocabulary that the field is assigned. But in the
+      // case that the field is not assigned a term, we must use the field name.
+      $field_name = $instance['field_name'];
+      $vocabulary = $instance['settings']['term_vocabulary'];
+      $accession = $instance['settings']['term_accession'];
+      $term = tripal_get_term_details($vocabulary, $accession);
+      if (!$term) {
+        continue;
+      }
+      $key = $term['name'];
+      $key_adj = strtolower(preg_replace('/ /', '_', $key));
+
+      // If this field should not be attached by default then just add a link
+      // so that the caller can get the information separately.
+      $instance_settings = $instance['settings'];
+      if (array_key_exists('auto_attach', $instance['settings']) and
+          $instance_settings['auto_attach'] == FALSE) {
+        // Add a URL only if there are values. If there are no values then
+        // don't add a URL which would make the end-user think they can get
+        // that information.
+        $items = field_get_items('TripalEntity', $entity, $field_name);
+        if ($items and count($items) > 0 and $items[0]['value']) {
+          $this->resource->addContextItem($key_adj, $term['url']);
+          $this->resource->addProperty($key_adj, $service_path . '/' . $entity->id . '/' . urlencode($term['name']));
+        }
+        else {
+          if ($hide_fields == 'show') {
+            $this->resource->addContextItem($key_adj, $term['url']);
+            $this->resource->addProperty($key_adj, NULL);
+          }
+        }
+        continue;
+      }
+
+      // Get the details for this field for the JSON-LD response.
+      $this->addEntityField($key_adj, $term, $entity, $bundle, $field, $instance, $service_path);
+    }
+  }
+
+  /**
+   * Adds the field as a property of the entity resource.
+   */
+  private function addEntityField($key, $term, $entity, $bundle, $field, $instance,
+      $service_path, $expfield = NULL) {
+
+    // If the entity is set to hide fields that have no values then we
+    // want to honor that in the web services too.
+    $hide_fields = tripal_get_bundle_variable('hide_empty_field', $bundle->id, 'hide');
+
+    // Get the field  settings.
+    $field_name = $field['field_name'];
+    $field_settings = $field['settings'];
+
+    $items = field_get_items('TripalEntity', $entity, $field_name);
+    if (!$items) {
+      return;
+    }
+
+    // Give modules the opportunity to edit values for web services. This hook
+    // really should be used sparingly. Where it helps is with non Tripal fields
+    // that are added to a TripalEntity content type and it doesn't follow
+    // the rules (e.g. Image field).
+    drupal_alter('tripal_ws_value', $items, $field, $instance);
+
+    $values = array();
+    for ($i = 0; $i < count($items); $i++) {
+      $values[$i] = $this->sanitizeFieldKeys($items[$i]['value'], $bundle, $service_path);
+    }
+
+    if ($hide_fields == 'hide' and empty($values[0])) {
+      return;
+    }
+
+    // If the field cardinality is 1
+    if ($field['cardinality'] == 1) {
+
+      // If the value is an array and this is the field page then all of those
+      // key/value pairs should be added directly to the response.
+      if (is_array($values[0])) {
+        if ($expfield) {
+          foreach ($values[0] as $k => $v) {
+            $this->resource->addProperty($k, $v);
+          }
+        }
+        else {
+          $this->resource->addContextItem($key, $term['url']);
+          $this->resource->addProperty($key, $values[0]);
+        }
+      }
+      // If the value is not an array it's a scalar so add it as is to the
+      // response.
+      else {
+        $this->resource->addContextItem($key, $term['url']);
+        $this->resource->addProperty($key, $values[0]);
+      }
+    }
+
+    // If the field cardinality is > 1
+    if ($field['cardinality'] != 1) {
+
+      // If this is the expanded field page then we need to swap out
+      // the resource for a collection.
+      $response = new TripalWebServiceCollection($service_path . '/' . urlencode($expfield));
+      $label = tripal_get_term_details('rdfs', 'label');
+      $response->addContextItem('label', $label['url']);
+      $response->addProperty('label', $instance['label']);
+      $i = 0;
+      foreach ($values as $delta => $element) {
+        $member = new TripalWebServiceResource($service_path . '/' . urlencode($expfield));
+        $member->setID($i);
+        // Add the context of the parent resource because all of the keys
+        // were santizied and set to match the proper context.
+        $member->setContext($this->resource);
+        $member->setType($key);
+        foreach ($element as $key => $value) {
+          $member->addProperty($key, $value);
+        }
+        $response->addMember($member);
+        $i++;
+      }
+      if ($expfield) {
+        $this->resource = $response;
+      }
+      else {
+        $this->resource->addProperty($key, $response);
+      }
+    }
+  }
+
+  /**
+   * Rewrites the keys of a field's items array for use with web services.
+   */
+  private function sanitizeFieldKeys($value, $bundle, $service_path) {
+    // If the entity is set to hide fields that have no values then we
+    // want to honor that in the web services too.
+    $hide_fields = tripal_get_bundle_variable('hide_empty_field', $bundle->id, 'hide');
+
+    $new_value = '';
+    // If the value is an array rather than a scalar then map the sub elements
+    // to controlled vocabulary terms.
+    if (is_array($value)) {
+      $temp = array();
+      foreach ($value as $k => $v) {
+        // exclude fields that have no values so we can hide them
+        if (empty($v) and $hide_fields == 'hide') {
+          continue;
+        }
+        $matches = array();
+        if (preg_match('/^(.+):(.+)$/', $k, $matches)) {
+          $vocabulary = $matches[1];
+          $accession = $matches[2];
+          $term = tripal_get_term_details($vocabulary, $accession);
+
+          $key_adj = strtolower(preg_replace('/ /', '_', $term['name']));
+          if (is_array($v)) {
+            $temp[$key_adj] = $this->sanitizeFieldKeys($v, $bundle, $service_path);
+          }
+          else {
+            $temp[$key_adj] = $v !== "" ? $v : NULL;
+          }
+          $this->resource->addContextItem($key_adj, $term['url']);
+
+        }
+        else {
+          // TODO: this is an error, if we get here then we have
+          // a key that isn't using the proper format... what to do?
+        }
+      }
+      $new_value = $temp;
+
+      // Recurse through the values array and set the entity elements
+      // and add the fields to the context.
+      $this->sanitizeFieldEntity($new_value, $service_path);
+
+    }
+    else {
+      $new_value = $value !== "" ? $value : NULL;
+    }
+
+    return $new_value;
+  }
+
+  /**
+   * Rewrites any TripalEntity elements in the values array for use with WS.
+   */
+  private function sanitizeFieldEntity(&$items, $service_path) {
+
+    if (!$items) {
+      return;
+    }
+    foreach ($items as $key => $value) {
+      if (is_array($value)) {
+        $this->sanitizeFieldEntity($items[$key], $service_path);
+        continue;
+      }
+
+      if ($key == 'entity') {
+        list($item_etype, $item_eid) = explode(':', $items['entity']);
+        if ($item_eid) {
+          $item_entity = tripal_load_entity($item_etype, array($item_eid));
+          $item_entity = reset($item_entity);
+          $bundle = tripal_load_bundle_entity(array('name' => $item_entity->bundle));
+          $items['@id'] = $this->getServicePath() . '/' . urlencode($bundle->label) . '/' . $item_eid;
+        }
+        unset($items['entity']);
+      }
+    }
+  }
+
+  /**
+   * Creates a collection of resources for a given type.
+   */
+  private function doContentTypeList($ctype) {
+    $service_path = $this->getServicePath() . '/' . urlencode($ctype);
+    $label = tripal_get_term_details('rdfs', 'label');
+    $this->resource = new TripalWebServiceCollection($service_path);
+    $this->resource->addContextItem('label', $label['url']);
+
+    // 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->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'];
+              $fterm = tripal_get_term_details($vocabulary, $accession);
+              $key = $fterm['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 ($this->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();
+
+    if (!$num_records) {
+      $num_records = 0;
+    }
+
+    // Add in the pager to the response.
+    $response['totalItems'] = $num_records;
+    $limit = array_key_exists('limit', $this->params) ? $this->params['limit'] : 25;
+
+    $total_pages = ceil($num_records / $limit);
+    $page = array_key_exists('page', $this->params) ? $this->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, $limit, $page);
+
+    // Iterate through the entities and add them to the list.
+    foreach ($results['TripalEntity'] as $entity_id => $stub) {
+      // 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();
+
+      $itemPage = tripal_get_term_details('schema', 'ItemPage');
+      $label = tripal_get_term_details('rdfs', 'label');
+      $member = new TripalWebServiceResource($service_path);
+      $member->addContextItem('label', $label['url']);
+      $member->addContextItem('ItemPage', $itemPage['url']);
+      $member->addContextItem($term->name, $term->url);
+      $member->setID($entity->id);
+      $member->setType($term->name);
+      $member->addProperty('label', $entity->title);
+      $member->addProperty('ItemPage', url('/bio_data/' . $entity->id, array('absolute' => TRUE)));
+      $this->resource->addMember($member);
+    }
+  }
+
+  /**
+   * Creates a resources that contains the list of content types.
+   */
+  private function doAllTypesList() {
+    $service_path = $this->getServicePath();
+    $label = tripal_get_term_details('rdfs', 'label');
+    $this->resource = new TripalWebServiceCollection($service_path);
+    $this->resource->addContextItem('label', $label['url']);
+    $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($service_path);
+      $member->addContextItem($term->name, $term->url);
+      $member->addContextItem('label', $label['url']);
+      $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);
+
+    }
+  }
+}

+ 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';
+}

+ 174 - 0
tripal_ws/includes/TripalWebServiceCollection.inc

@@ -0,0 +1,174 @@
+<?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;
+
+  /**
+   * The current page of the pager.
+   */
+  protected $page;
+
+
+  /**
+   * Implements the constructor.
+   *
+   * @param TripalWebService $service
+   *   An instance of a TripalWebService or class that extends it.
+   */
+  public function __construct($service_path) {
+    parent::__construct($service_path);
+    $this->members = array();
+    $term = tripal_get_term_details('hydra', 'Collection');
+    $this->addContextItem('Collection', $term['url']);
+    $term = tripal_get_term_details('hydra', 'totalItems');
+    $this->addContextItem('totalItems', $term['url']);
+    $term = tripal_get_term_details('hydra', 'member');
+    $this->addContextItem('member', $term['url']);
+    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.
+   * @param $path
+   *   The path
+   */
+  public function initPager($totalItems, $itemsPerPage, $page) {
+    $this->doPaging = TRUE;
+    $this->totalItems = $totalItems;
+    $this->itemsPerPage = $itemsPerPage;
+    $this->page = $page;
+  }
+
+  /**
+   * 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 = $this->page;
+      $limit = $this->itemsPerPage;
+
+      if ($this->totalItems > 0) {
+        $data['view'] = array(
+          '@id' => $this->service_path . '?' . implode('&', array_merge(array("page=$page", "limit=$limit"))),
+          '@type' => 'PartialCollectionView',
+          'first' => $this->service_path . '?' . implode('&', array_merge(array("page=1", "limit=$limit"))),
+          'last' => $this->service_path . '?' . implode('&', array_merge(array("page=$total_pages", "limit=$limit"))),
+        );
+        $prev = $page - 1;
+        $next = $page + 1;
+        if ($prev > 0) {
+          $data['view']['previous'] = $this->service_path .'?' . implode('&', array("page=$prev", "limit=$limit"));
+        }
+        if ($next < $total_pages) {
+          $data['view']['next'] = $this->service_path . '?' . implode('&', array("page=$next", "limit=$limit"));
+        }
+      }
+    }
+    else {
+      $data['totalItems'] = count($this->members);
+    }
+
+    $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;
-  }
-}

+ 254 - 0
tripal_ws/includes/TripalWebServiceResource.inc

@@ -0,0 +1,254 @@
+<?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;
+
+
+  /**
+   * The URL path that the service is providing to access this resource.
+   * This path plus the $id are concatenated to form the IRI for this resource.
+   */
+  protected $service_path;
+
+
+  /**
+   * Implements the constructor.
+   *
+   * @param TripalWebService $service
+   *   An instance of a TripalWebService or class that extends it.
+   */
+  public function __construct($service_path) {
+    $this->context = array();
+    $this->data = array();
+    $this->service_path = $service_path;
+
+    // First, add the RDFS and Hydra vocabularies to the context.  All Tripal
+    // web services should use these.
+    $vocab = tripal_get_vocabulary_details('rdfs');
+    $this->addContextItem('rdfs', $vocab['url']);
+
+    $vocab = tripal_get_vocabulary_details('hydra');
+    $this->addContextItem('hydra', $vocab['url']);
+
+    $vocab = tripal_get_vocabulary_details('dc');
+    $this->addContextItem('dc', $vocab['url']);
+
+    $vocab = tripal_get_vocabulary_details('schema');
+    $this->addContextItem('schema', $vocab['url']);
+
+    $vocab = tripal_get_vocabulary_details('local');
+    $this->addContextItem('local', $vocab['url']);
+
+    $this->data['@id'] = $service_path;
+    $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'] = $this->service_path . '/' . $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;
+  }
+
+  /**
+   * Copies the context from a given TripalWebService resource to this
+   * resource.
+   *
+   * @param $resource
+   */
+  public function setContext($resource) {
+    if (!is_a($resource, 'TripalWebServiceResource')) {
+      throw new Exception("The \$resource argument must be an instance of a TripalWebServiceResource.");
+    }
+    $this->context = $resource->getContext();
+  }
+
+}

+ 133 - 1
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,126 @@ function tripal_ws_menu() {
   return $items;
 }
 
+/**
+ * The callback function for all RESTful web services.
+ *
+ */
+function tripal_ws_get_services() {
+  global $base_url;
+  $service_path = $base_url . '/web-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($service_path);
+        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($args);
+    $service->handleRequest();
+    $response = $service->getResponse();
+    print drupal_json_encode($response);
+
+  }
+  catch (Exception $e) {
+    $service = new TripalWebService($service_path);
+    $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;
+  $base_path = $base_url . '/web-services';
+
+  // Create an instance of the TriaplWebService class and use it to build
+  // the entry point for the web serivces.
+  $service = new TripalWebService($base_path);
+
+  // Get the list of web service classes.
+  $services = tripal_get_web_services();
+
+  // Create the parent resource which is a collection.
+  $resource = new TripalWebServiceResource($base_path);
+  $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($base_path);
+    $version = $service->getVersion();
+    $resource->addContextItem($service_class::$type, '');
+    $resource->addProperty($service_class::$type, $service->getServicePath());
+  }
+
+  // For discoverability add the document webservice.
+  $service->setResource($resource);
+  $response = $service->getResponse();
+  print drupal_json_encode($response);
+
+
+
+}
 /**
  * The callback function for all RESTful web services.
  *
@@ -173,7 +305,7 @@ function tripal_ws_load_remote_entity($site_id, $api_version, $ctype, $id) {
   drupal_set_title($response['label']);
 
   // Attribute this data to the proper source.
-  $source_url = l($response['label'], $response['itemPage'], array('attributes' => array('target' => '_blank')));
+  $source_url = l($response['label'], $response['ItemPage'], array('attributes' => array('target' => '_blank')));
   $content = '<div><strong>Source:</strong> ' . $site->name . ': ' . $source_url . '</div>';
 
   // Fake an entity so we can display this content using the same