addDocBundleClasses(); $this->addDocContentCollectionClass(); return parent::getDocumentation(); } /** * @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] : ''; // is this a valid content type? if ($ctype) { // Get the list of published terms (these are the bundle IDs) $bquery = db_select('tripal_bundle', 'tb'); $bquery->join('tripal_term', 'tt', 'tt.id = tb.term_id'); $bquery->join('tripal_vocab', 'tv', 'tv.id = tt.vocab_id'); $bquery->fields('tb', ['label']); $bquery->fields('tt', ['accession']); $bquery->fields('tv', ['vocabulary']); $bquery->orderBy('tb.label', 'ASC'); $bundles = $bquery->execute(); // Iterate through the terms convert the santized name to the real label. $i = 0; $ctype_lookup = []; $found = FALSE; while ($bundle = $bundles->fetchObject()) { // Check the label by replacing non alpha-numeric characters with // an underscore and is case-insensitive $label = preg_replace('/[^\w]/', '_', $bundle->label); if (preg_match("/^$label$/i", $ctype)) { $ctype = $bundle->label; $found = TRUE; } // Check if this is an accession. if ($ctype == $bundle->vocabulary . ':' . $bundle->accession) { $ctype = $bundle->label; $found = TRUE; } } if (!$found) { throw new Exception('Invalid content type: ' . $ctype); } } // If we have a content type then list all of the entities that belong // to it. if ($ctype and !$entity_id and !$expfield) { $this->doEntityList($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->doContentTypesList(); } } } } /** * Creates a resource for an expanded field of an entity. */ private function doExpandedField($ctype, $entity_id, $expfield) { $service_path = $this->getServicePath() . '/' . preg_replace('/[^\w]/', '_', $ctype) . '/' . $entity_id; $this->resource = new TripalWebServiceResource($service_path); // Get the TripalBundle, TripalTerm and TripalVocab for this type. $bundle = tripal_load_bundle_entity(['label' => $ctype]); // Find the field that matches the field name provided by the user. list($field, $instance, $term) = $this->findField($bundle, $expfield); if (!$field) { throw new Exception("Could not find a matching field for the name: $expfield"); } // Get the TripalEntity $entity = tripal_load_entity('TripalEntity', ['id' => $entity_id], FALSE, [$field['id']]); $entity = reset($entity); // If we couldn't find the entity then fail. if (!$entity) { throw new Exception("Cannot find the record with id $entity_id."); } // Check that the user has access to this entity. If not then the // function call will throw an error. $this->checkAccess($entity); // Next add in the ID and Type for this resources. $this->setResourceType($this->resource, $term); $this->resource->setID(urlencode($term['name'])); if (!property_exists($entity, $field['field_name'])) { // Attach the field and then add its values to the response. field_attach_load($entity->type, [$entity->id => $entity], FIELD_LOAD_CURRENT, ['field_id' => $field['id']]); } $this->addEntityField($this->resource, $term, $entity, $bundle, $field, $instance, $service_path, $expfield); } /** * Find the field whose term matches the one provied. */ private function findField($bundle, $expfield) { $value = []; $instances = field_info_instances('TripalEntity', $bundle->name); foreach ($instances as $instance) { $field_name = $instance['field_name']; $field = field_info_field($field_name); $field_type = $field['type']; // Skip fields of remote data. if ($field_type == 'remote__data') { continue; } $vocabulary = $instance['settings']['term_vocabulary']; $accession = $instance['settings']['term_accession']; $temp_term = tripal_get_term_details($vocabulary, $accession); // See if the name matches perfectly. if (strtolower($temp_term['name']) == strtolower($expfield)) { return [$field, $instance, $temp_term]; } // See if the name provided matches the field name after a bit of // cleanup. if (strtolower(preg_replace('/[^\w]/', '_', $temp_term['name'])) == strtolower($expfield)) { return [$field, $instance, $temp_term]; } // Alternatively if the CV term accession matches then we're good too. if ($vocabulary . ':' . $accession == $expfield) { return [$field, $instance, $temp_term]; } } } /** * Creates a resource for a single entity. */ private function doEntity($ctype, $entity_id) { $service_path = $this->getServicePath() . '/' . preg_replace('/[^\w]/', '_', $ctype); $this->resource = new TripalWebServiceResource($service_path); // Get the TripalBundle, TripalTerm and TripalVocab type for this type. $bundle = tripal_load_bundle_entity(['label' => $ctype]); $term = entity_load('TripalTerm', ['id' => $bundle->term_id]); $term = reset($term); // Convert the $term to a simple array $term = tripal_get_term_details($term->vocab->vocabulary, $term->accession); // Add the vocabulary to the context. $this->resource->addContextItem($term['name'], $term['url']); // Get the TripalEntity. $entity = tripal_load_entity('TripalEntity', ['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("Cannot find this record."); } // Check that the user has access to this entity. If not then the // function call will throw an error. $this->checkAccess($entity); $itemPage = tripal_get_term_details('schema', 'ItemPage'); $label = tripal_get_term_details('rdfs', 'label'); $this->resource->setID($entity_id); $this->setResourceType($this->resource, $term); $this->addResourceProperty($this->resource, $label, $entity->title); $this->addResourceProperty($this->resource, $itemPage, url('/bio_data/' . $entity->id, ['absolute' => TRUE])); // Add in the entitie's fields. $this->addEntityFields($this->resource, $entity, $bundle, $term, $service_path); } /** * Ensures that user's only have access to content they should see. * * Denies access to an entity if it is unpublished or if the user does * not have permission to see it. * * @param $entity * The full entity object. * * @throws Exception */ private function checkAccess($entity) { global $user; if (!tripal_entity_access('view', $entity, $user, 'TripalEntity')) { throw new Exception("Permission Denied."); } // Don't show entities that aren't published if ($entity->status == 0) { throw new Exception("This record is currently unavailable."); } } /** * Adds the fields as properties of an entity resource. */ private function addEntityFields($resource, $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); // 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); // Sort the instances by their weight. 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); // If the field has the $no_data turned on then we should exclude it. if (tripal_load_include_field_class($field['type'])) { $field_class = $field['type']; if ($field_class::$no_data) { return; } } // Skip the remote__data field that is provided by the tripal_ws // module. if ($field['type'] == 'remote__data') { continue; } // 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; } // 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); $term_key = $this->getContextTerm($term, ['lowercase', 'spacing']); $resource->addContextItem($term_key, $vocabulary . ':' . $accession); $resource->addContextItem($vocabulary . ':' . $accession, [ '@id' => $term['url'], '@type' => '@id', ]); if ($items and count($items) > 0 and $items[0]['value']) { $this->addResourceProperty($resource, $term, $service_path . '/' . $entity->id . '/' . urlencode($term['name']), [ 'lowercase', 'spacing', ]); } else { if ($hide_fields == FALSE) { $this->addResourceProperty($resource, $term, NULL, [ 'lowercase', 'spacing', ]); } } continue; } // Get the details for this field for the JSON-LD response. $this->addEntityField($resource, $term, $entity, $bundle, $field, $instance, $service_path); } } /** * Adds the field as a property of the entity resource. */ private function addEntityField($resource, $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); // 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 = []; for ($i = 0; $i < count($items); $i++) { if (array_key_exists('value', $items[$i])) { $values[$i] = $this->sanitizeFieldKeys($resource, $items[$i]['value'], $bundle, $service_path); } elseif ($field['type'] == 'image') { $url = file_create_url($items[$i]['uri']); $values[$i] = $this->sanitizeFieldKeys($resource, $url, $bundle, $service_path); } else { // TODO: handle this case. } } if (isset($field['field_permissions']['type'])) { global $user; if ($field['field_permissions']['type'] === '1') { // Field is private. if (!user_access('view ' . $field_name, $user)) { return; } } if ($field['field_permissions']['type'] === '2') { // Field is custom permissions: Check // if user has access to view this field in any entity. if (!user_access('view ' . $field_name, $user)) { return; } } } if ($hide_fields == TRUE 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) { $resource->addProperty($k, $v); } } else { $this->addResourceProperty($resource, $term, $values[0], [ 'lowercase', 'spacing', ]); } } // If the value is not an array it's a scalar so add it as is to the // response. else { $this->addResourceProperty($resource, $term, $values[0], [ 'lowercase', 'spacing', ]); } } // If the field cardinality is > 1 or -1 (for unlimited) 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), $this->params); $label = tripal_get_term_details('rdfs', 'label'); $this->addResourceProperty($response, $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($resource); $this->setResourceType($member, $term); foreach ($element as $key => $value) { $member->addProperty($key, $value); } $response->addMember($member); $i++; } if ($expfield) { $this->resource = $response; } else { //$this->resource->addProperty($key, $response); $this->addResourceProperty($resource, $term, $response, [ 'lowercase', 'spacing', ]); } } } /** * Rewrites the keys of a field's items array for use with web services. */ private function sanitizeFieldKeys($resource, $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); $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 = []; foreach ($value as $k => $v) { // exclude fields that have no values so we can hide them if (!isset($v) and $hide_fields == TRUE) { continue; } $matches = []; if (preg_match('/^(.+):(.+)$/', $k, $matches)) { $vocabulary = $matches[1]; $accession = $matches[2]; $term = tripal_get_term_details($vocabulary, $accession); $key = $this->addContextTerm($resource, $term, [ 'lowercase', 'spacing', ]); if (is_array($v)) { $temp[$key] = $this->sanitizeFieldKeys($resource, $v, $bundle, $service_path); } else { $temp[$key] = $v; } $term['name'] = $key; } 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; } 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, [$item_eid]); $item_entity = reset($item_entity); $bundle = tripal_load_bundle_entity(['name' => $item_entity->bundle]); $items['@id'] = $this->getServicePath() . '/' . urlencode($bundle->label) . '/' . $item_eid; } unset($items['entity']); } } } /** * A helper function to make it easy to map between keys and their fields. * * @bundle * The bundle object. Fields attached to this bundle will be included * in the mapping array. * @return * An associative arrray that maps web servcies keys to fields and * fields to web services keys (reciprocol). */ private function getFieldMapping($bundle) { // 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 = []; $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']; } } } } } return $field_mapping; } /** * Gets any order by statements provided by the user. * * @field_mapping * An array that maps WS keys to field names. As provided by the * getFieldMapping() function. * @return * An array of fields for ordering. * * @throws Exception */ private function getOrderBy($field_mapping, $bundle) { $order_by = []; // Handle order separately. if (array_key_exists('order', $this->params)) { $order_params = $this->params['order']; $dir = 'ASC'; // If the user provided more than one order statement then those are // separated by a semicolon. $items = explode(';', $order_params); foreach ($items as $key) { // The user can provide a direction by separating the field key and the // direction with a '|' character. $matches = []; if (preg_match('/^(.*)\|(.*)$/', $key, $matches)) { $key = $matches[1]; if ($matches[2] == 'ASC' or $matches[2] == 'DESC') { $dir = $matches[2]; } else { throw new Exception('Please provide "ASC" or "DESC" for the ordering direction'); } } // Break apart any subkeys and pull the first one as this is the parent // field. $subkeys = explode(',', $key); if (count($subkeys) > 0) { $key = $subkeys[0]; } if (array_key_exists($key, $field_mapping)) { $key_field_name = $field_mapping[$key]; $key_field = field_info_field($key_field_name); $key_instance = field_info_instance('TripalEntity', $key_field_name, $bundle->name); // Complex fields provied by the TripalField class may have sub // elements that support filtering. We need to see if the user // wants to filter on those. $field_class = $key_field['type']; if (tripal_load_include_field_class($field_class)) { // To find out which fields are sortable we'll call the // webServicesData() function. $key_field = new $field_class($key_field, $key_instance); $ws_data = $key_field->webServicesData(); $sortable_keys = $ws_data['sortable']; $criteria = implode('.', $subkeys); if (array_key_exists($criteria, $sortable_keys)) { $order_by[$key_field_name][] = [ 'column' => $sortable_keys[$criteria], 'dir' => $dir, ]; } else { throw new Exception("The value, '$criteria', is not available for sorting."); } } // If this field is not a TripalField then it should just have // a simple value and we can query for that. else { $key_field_id = $key_instance['settings']['term_vocabulary'] . ':' . $key_instance['settings']['term_accession']; $order_by[$key_field_name][] = [ 'column' => $key_field_id, 'dir' => $dir, ]; } } else { throw new Exception("The value, '$key', is not available for sorting."); } } } // If there is no ordering that is set then set a default order. if (count(array_keys($order_by)) == 0) { $key_field_names = []; if (in_array('data__identifier', $field_mapping)) { $key_field_names['data__identifier'][] = 'identifier'; } else { if (in_array('schema__name', $field_mapping)) { $key_field_names['schema__name'][] = 'name'; } else { if (in_array('rdfs_label', $field_mapping)) { $key_field_names['rdfs_label'][] = 'label'; } else { if (in_array('taxrank__genus', $field_mapping)) { $key_field_names['taxrank__genus'][] = 'genus'; $key_field_names['taxrank__species'][] = 'species'; } } } } foreach ($key_field_names as $key_field_name => $criteria) { $key_field = field_info_field($key_field_name); $key_instance = field_info_instance('TripalEntity', $key_field_name, $bundle->name); $key_field_id = $key_instance['settings']['term_vocabulary'] . ':' . $key_instance['settings']['term_accession']; $field_class = $key_field['type']; if (tripal_load_include_field_class($field_class)) { // To find out which fields are sortable we'll call the // webServicesData() function. $key_field = new $field_class($key_field, $key_instance); $ws_data = $key_field->webServicesData(); $sortable_keys = $ws_data['sortable']; if (array_key_exists($criteria, $sortable_keys)) { $order_by[$key_field_name][] = [ 'column' => $sortable_keys[$criteria], 'dir' => $dir, ]; } } // If this field is not a TripalField then it should just have // a simple value and we can query for that. else { $order_by[$key_field_name][] = [ 'column' => $key_field_id, 'dir' => 'ASC', ]; } } } return $order_by; } /** * Gets any filter by statements provided by the user. * * @field_mapping * An array that maps WS keys to field names. As provided by the * getFieldMapping() function. * * @return * An array of fields for filtering. * * @throws Exception */ private function getFieldFilters($field_mapping, $bundle) { $filters = []; // Iterate through the paramter list provided by user. foreach ($this->params as $param => $value) { // Ignore non filter parameters. if ($param == 'page' or $param == 'limit' or $param == 'order' or $param == 'ids' or $param == 'fields') { continue; } // Break apart any operators $key = $param; $op = '='; $matches = []; if (preg_match('/^(.+);(.+)$/', $key, $matches)) { $key = $matches[1]; $op = $matches[2]; } // Break apart any subkeys and pull the first one as this is the parent // field. $subkeys = explode(',', $key); if (count($subkeys) > 0) { $key = $subkeys[0]; } // Map the values in the filters to their appropriate field names. if (array_key_exists($key, $field_mapping)) { $key_field_name = $field_mapping[$key]; $key_field = field_info_field($key_field_name); $key_instance = field_info_instance('TripalEntity', $key_field_name, $bundle->name); // Complex fields provied by the TripalField class may have sub // elements that support filtering. We need to see if the user // wants to filter on those. $field_class = $key_field['type']; if (tripal_load_include_field_class($field_class)) { // To find out which fields are searchable we'll call the wsData() // function. $key_field = new $field_class($key_field, $key_instance); $ws_data = $key_field->webServicesData(); $searchable_keys = $ws_data['searchable']; $criteria = implode('.', $subkeys); if (array_key_exists($criteria, $searchable_keys)) { $filters[$key_field_name][] = [ 'value' => $value, 'op' => $op, 'column' => $searchable_keys[$criteria], ]; } else { throw new Exception("The filter term, '$criteria', is not available for use."); } } // If this field is not a TripalField then it should just have // a simple value and we can query for that. else { $key_field_id = $key_instance['settings']['term_vocabulary'] . ':' . $key_instance['settings']['term_accession']; $filters[$key_field_name][] = [ 'value' => $value, 'op' => $op, 'column' => $key_field_id, ]; } } else { throw new Exception("The filter term, '$key', is not available for use."); } } // Now convert the operation for each filter to one that is compatible // with TripalFieldQuery. foreach ($filters as $key_field_name => $key_filters) { foreach ($key_filters as $i => $filter) { $op = '='; switch ($filters[$key_field_name][$i]['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 = '='; } $filters[$key_field_name][$i]['op'] = $op; } } return $filters; } /** * Creates a collection of resources for a given type. */ private function doEntityList($ctype) { $service_path = $this->getServicePath() . '/' . preg_replace('/[^\w]/', '_', $ctype); $this->resource = new TripalWebServiceCollection($service_path, $this->params); // Get the TripalBundle, TripalTerm and TripalVocab type for this type. $bundle = tripal_load_bundle_entity(['label' => $ctype]); $term = entity_load('TripalTerm', ['id' => $bundle->term_id]); $term = reset($term); // The type of collection is provided by our API vocabulary service. $vocab_service = new TripalDocService_v0_1($this->base_path); $this->resource->addContextItem('vocab', $vocab_service->getServicePath() . '#'); $accession = preg_replace('/[^\w]/', '_', $bundle->label . ' Collection'); $this->resource->addContextItem($accession, 'vocab:' . $accession); $this->resource->setType($accession); // Convert term to a simple array $term = tripal_get_term_details($term->vocab->vocabulary, $term->accession); // Set the label for this collection. $label = tripal_get_term_details('rdfs', 'label'); $this->addResourceProperty($this->resource, $label, $bundle->label . " Collection"); // For quick lookup, get the mapping of WS keys to their appropriate fields. $field_mapping = $this->getFieldMapping($bundle); // Get arrays for filters and order by statements. $filters = $this->getFieldFilters($field_mapping, $bundle); $order_by = $this->getOrderBy($field_mapping, $bundle); // Initialize the query to search for records for our bundle types // that are published. $query = new TripalFieldQuery(); $query->entityCondition('entity_type', 'TripalEntity'); $query->entityCondition('bundle', $bundle->name); $query->propertyCondition('status', 1); if (array_key_exists('ids', $this->params)) { $eids = explode(',', $this->params['ids']); if (count($eids) > 1000) { throw new Exception('Please provide no more than 1000 ids.'); } if (!is_numeric(implode('', $eids))) { throw new Exception('All supplied ids must be numeric.'); } $query->entityCondition('entity_id', $eids, 'IN'); } // Now iterate through the filters and add those. foreach ($filters as $key_field_name => $key_filters) { foreach ($key_filters as $i => $filter) { $column_name = $filter['column']; $value = $filter['value']; $op = $filter['op']; $query->fieldCondition($key_field_name, $column_name, $value, $op); } } // Now set the order by. foreach ($order_by as $key_field_name => $key_order) { foreach ($key_order as $i => $order) { $column_name = $order['column']; $dir = $order['dir']; $query->fieldOrderBy($key_field_name, $column_name, $dir); } } // 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 range $start = ($page - 1) * $limit; $query->range($start, $limit); // Now perform the query. $results = $query->execute(); $this->resource->initPager($num_records, $limit, $page); // Check to make sure there are results. $entity_ids = []; if (isset($results['TripalEntity']) AND is_array($results['TripalEntity'])) { $entity_ids = $results['TripalEntity']; } // If the user wants to include any fields in the list then those provided // names need to be converted to fields. $add_fields = []; $add_field_ids = []; if (array_key_exists('fields', $this->params)) { $fields = explode(',', $this->params['fields']); foreach ($fields as $expfield) { list($field, $instance, $temp_term) = $this->findField($bundle, $expfield); if ($field) { $add_fields[$expfield]['field'] = $field; $add_fields[$expfield]['instance'] = $instance; $add_fields[$expfield]['term'] = $temp_term; $add_field_ids[] = $field['id']; } else { throw new Exception(t('The field named, "!field", does not exist.', ['!field' => $expfield])); } } } // Iterate through the entities and add them to the output list. foreach ($entity_ids 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', ['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->setID($entity->id); $this->setResourceType($member, $term); $this->addResourceProperty($member, $label, $entity->title); $this->addResourceProperty($member, $itemPage, url('/bio_data/' . $entity->id, ['absolute' => TRUE])); $entity = tripal_load_entity('TripalEntity', [$entity_id], FALSE, $add_field_ids); $entity = $entity[$entity_id]; // Add in any requested fields foreach ($add_fields as $expfield => $expfield_details) { $this->addEntityField($member, $expfield_details['term'], $entity, $bundle, $expfield_details['field'], $expfield_details['instance'], $service_path); } $this->resource->addMember($member); } } /** * Creates a resources that contains the list of content types. */ private function doContentTypesList() { $service_path = $this->getServicePath(); $service_vocab = new TripalDocService_v0_1($this->base_path); $this->resource = new TripalWebServiceCollection($service_path, $this->params); $this->resource->addContextItem('vocab', $service_vocab->getServicePath()); $this->resource->addContextItem('Content_Collection', $service_vocab->getServicePath() . '#Content_Collection'); $this->resource->setType('Content_Collection'); $label = tripal_get_term_details('rdfs', 'label'); $this->addResourceProperty($this->resource, $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', ['id' => $bundle->term_id]); $term = reset($entity); $vocab = $term->vocab; // Convert the term to a simple array $term = tripal_get_term_details($term->vocab->vocabulary, $term->accession); $member = new TripalWebServiceResource($service_path); $member->setID(preg_replace('/[^\w]/', '_', $bundle->label)); $vocab_service = new TripalDocService_v0_1($this->base_path); $member->addContextItem('vocab', $vocab_service->getServicePath() . '#'); $accession = preg_replace('/[^\w]/', '_', $bundle->label . ' Collection'); $member->addContextItem($accession, 'vocab:' . $accession); $member->setType($accession); $this->addResourceProperty($member, $label, $bundle->label . ' Collection'); $member->addContextItem('description', 'rdfs:comment'); // Get the bundle description. If no description is provided then // use the term definition $description = trim(tripal_get_bundle_variable('description', $bundle->id)); if (!$description) { $description = $term['definition']; } if (!$description) { $description = ''; } $member->addProperty('description', 'A collection of ' . $bundle->label . ' resources: ' . lcfirst($description)); $this->resource->addMember($member); } } /** * Adds the content collection class to the document for this service. */ private function addDocContentCollectionClass() { $details = [ 'id' => 'vocab:Content_Collection', 'term' => 'vocab:Content_Collection', 'title' => 'Content Collection', ]; $vocab = tripal_get_vocabulary_details('hydra'); $properties = []; $properties[] = [ 'type' => $vocab['sw_url'], 'title' => 'member', 'description' => "The list of available content types.", "required" => NULL, "readonly" => FALSE, "writeonly" => FALSE, ]; $properties[] = [ "type" => $vocab['sw_url'], "title" => "totalItems", "description" => "The total number of content types.", "required" => NULL, "readonly" => FALSE, "writeonly" => FALSE, ]; $properties[] = [ "type" => $vocab['sw_url'], "title" => "label", "description" => "The type content.", "required" => NULL, "readonly" => FALSE, "writeonly" => FALSE, ]; $operations = []; $operations['GET'] = [ 'label' => 'Retrieves a collection (a list) of available content types.', 'type' => '_:content_collection_retrieve', 'expects' => NULL, 'returns' => 'vocab:ContentCollection', ]; $this->addDocClass($details, $operations, $properties); } /** * Adds classes for every content type to the documentation for this service. */ private function addDocBundleClasses() { global $user; // 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 content types and add a class for each one. $i = 0; while ($bundle = $bundles->fetchObject()) { $entity = entity_load('TripalTerm', ['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->getDefinition(); } // Create the details array for the class. $class_id = $this->getServicePath() . '/' . urlencode($bundle->label); $details = [ 'id' => $term->getURL(), 'term' => $term->getAccession(), 'title' => preg_replace('/[^\w]/', '_', $bundle->label), 'description' => $description, ]; // Add in the supported operations for this content type. $operations = []; // If the user can view this content type. if (user_access('view ' . $bundle->name)) { $label = "Retrieves the " . $bundle->label . " resource."; $operations['GET'] = [ 'label' => $label, 'description' => NULL, 'returns' => $term->url, 'type' => '_:' . preg_replace('/[^\w]/', '_', strtolower($bundle->label)) . '_retrieve', ]; } // If the user can edit this content type. if (user_access('edit ' . $bundle->name)) { $label = "Update and replace the " . $bundle->label . " resource."; if (preg_match('/^[aeiou]/i', $bundle->label)) { $label = "Update and replace an " . $bundle->label . " resource."; } // TODO: add this back in when web services support this method. // $operations['PUT'] = array( // 'label' => $label, // 'description' => NULL, // 'returns' => $term->url, // 'type' => '_:' . preg_replace('/[^\w]/', '_', strtolower($bundle->label)) . '_update', // ); } // If the user can edit this content type. if (user_access('delete ' . $bundle->name)) { $label = "Deletes the " . $bundle->label . " resource."; if (preg_match('/^[aeiou]/i', $bundle->label)) { $label = "Deletes an " . $bundle->label . " resource."; } // TODO: add this back in when web services support this method. // $operations['DELETE'] = array( // 'label' => $label, // 'description' => NULL, // 'returns' => $term->url, // 'type' => '_:' . preg_replace('/[^\w]/', '_', strtolower($bundle->label)) . '_delete', // ); } // Add in the properties that correspond to fields in the data. $properties = $this->addDocBundleFieldProperties($bundle, $term); $this->addDocClass($details, $operations, $properties); // Now add the bundle collection class. $this->addDocBundleCollectionClass($bundle, $term); } // end while ($bundle = $bundles->fetchObject()) { ... } /** * Every content type (bundle) has fields that need to be set as properties. */ private function addDocBundleFieldProperties($bundle, $bundle_term) { $properties = []; $content_type_accession = $bundle_term->vocab->vocabulary . ':' . $bundle_term->accession; $instances = field_info_instances('TripalEntity', $bundle->name); foreach ($instances as $instance) { // Skip deleted fields. if ($instance['deleted']) { continue; } // Skip hidden fields. if ($instance['display']['default']['type'] == 'hidden') { continue; } $accession = $instance['settings']['term_vocabulary'] . ":" . $instance['settings']['term_accession']; $field_name = $instance['field_name']; $field = field_info_field($field_name); $field_type = $field['type']; // Skip fields of remote data. if ($field_type == 'remote__data') { continue; } // Check if this field is an auto attach. If not, then we have alink and // we need to indicate that the link has operations. $proptype = $instance['settings']['term_vocabulary'] . ':' . $instance['settings']['term_accession']; if ($instance['settings']['auto_attach'] == FALSE) { // Create a WebServiceResource for the hydra:Link type. $id = $content_type_accession . '/' . $accession; $link = new TripalWebServiceResource($this->base_path); $link->setID($accession); $link->setType('hydra:Link'); $link->addContextItem('domain', [ "@id" => "rdfs:domain", "@type" => "@id", ]); $link->addContextItem('range', [ "@id" => "rdfs:range", "@type" => "@id", ]); $link->addContextItem('readable', 'hydra:readable'); $link->addContextItem('writeable', 'hydra:writeable'); $link->addContextItem('required', 'hydra:required'); $link->addContextItem('description', 'rdfs:comment'); $link->addContextItem('label', 'rdfs:label'); $link->addProperty('hydra:title', $instance['label']); $link->addProperty('hydra:description', $instance['description']); // $link->addProperty('domain', $service_path . '#EntryPoint'); // $link->addProperty('range', $service_class::$label); $ops = []; $op = new TripalWebServiceResource($this->base_path); $op->setID('_:' . $field_name . '_retrieve'); $op->setType('hydra:Operation'); $op->addContextItem('method', 'hydra:method'); $op->addContextItem('label', 'rdfs:label'); $op->addContextItem('description', 'rdfs:comment'); $op->addContextItem('expects', [ "@id" => "hydra:expects", "@type" => "@id", ]); $op->addContextItem('returns', [ "@id" => "hydra:returns", "@type" => "@id", ]); $op->addContextItem('statusCodes', 'hydra:statusCodes'); $op->addProperty('method', "GET"); $op->addProperty('label', 'Retrieves the ' . $instance['label'] . ' resource.'); $op->addProperty('description', $instance['description']); $op->addProperty('expects', NULL); $op->addProperty('returns', $accession); $op->addProperty('statusCodes', []); $ops[] = $op; $link->addContextItem('supportedOperation', 'hydra:supportedOperation'); $link->addProperty('supportedOperation', $ops); $proptype = $link; } $formatters = tripal_get_field_field_formatters($field, $instance); $property = [ 'type' => $proptype, 'title' => $instance['label'], 'description' => $instance['description'], "required" => $instance['required'] ? TRUE : FALSE, "readonly" => FALSE, "writeonly" => TRUE, "tripal_formatters" => $formatters, ]; $properties[] = $property; } return $properties; } /** * Every content type (bundle) needs a collection class in the documentation. */ private function addDocBundleCollectionClass($bundle, $term) { $accession = preg_replace('/[^\w]/', '_', $bundle->label . ' Collection'); $details = [ 'id' => 'vocab:' . $accession, 'term' => 'vocab:' . $accession, 'title' => $bundle->label . ' Collection', 'subClassOf' => 'hydra:Collection', 'description' => 'A collection (or list) of ' . $bundle->label . ' resources.', ]; $vocab = tripal_get_vocabulary_details('hydra'); $properties = []; $properties[] = [ 'type' => $vocab['sw_url'], 'title' => 'member', 'description' => "The list of available " . $bundle->label . '(s).', "required" => NULL, "readonly" => FALSE, "writeonly" => FALSE, ]; $properties[] = [ "type" => $vocab['sw_url'], "title" => "totalItems", "description" => "The total number of resources.", "required" => NULL, "readonly" => FALSE, "writeonly" => FALSE, ]; $properties[] = [ "type" => $vocab['sw_url'], "title" => "label", "description" => "A label or name for the resource.", "required" => NULL, "readonly" => FALSE, "writeonly" => FALSE, ]; $class_id = $this->getServicePath() . '/' . urlencode($bundle->label); $operations = []; $operations['GET'] = [ 'label' => 'Retrieves a list of all ' . $bundle->label . ' resources.', 'description' => NULL, 'expects' => NULL, 'returns' => $term->url, 'type' => '_:' . preg_replace('/[^\w]/', '_', strtolower($bundle->label)) . '_collection_retrieve', ]; // If the user can create this content type then we allow a POST on the // collection type. if (user_access('create ' . $bundle->name)) { $label = "Creates a " . $bundle->label; if (preg_match('/^[aeiou]/i', $bundle->label)) { $label = "Creates an " . $bundle->label; } // TODO: add this back in when web services support this method. // $operations['POST'] = array( // 'label' => $label, // 'description' => NULL, // 'expects' => $term->url, // 'returns' => $term->url, // 'type' => '_:' . preg_replace('/[^\w]/', '_', strtolower($bundle->label)) . '_create', // 'statusCodes' => array( // array( // "code" => 201, // "description" => "If the " . $bundle->label . " was created successfully." // ), // ), // ); } $this->addDocClass($details, $operations, $properties); } }