Mercurial > hg > rr-repo
diff sites/all/modules/restws/restws.formats.inc @ 4:ce11bbd8f642
added modules
author | danieleb <danielebarchiesi@me.com> |
---|---|
date | Thu, 19 Sep 2013 10:38:44 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sites/all/modules/restws/restws.formats.inc Thu Sep 19 10:38:44 2013 +0100 @@ -0,0 +1,788 @@ +<?php + +/** + * @file + * RESTful web services module formats. + */ + +/** + * Interface implemented by formatter implementations for the http client. + */ +interface RestWSFormatInterface { + + /** + * Gets the representation of a resource. + * + * @param RestWSResourceControllerInterface $resourceController + * The controller used to retrieve the resource. + * @param int|string $id + * The id of the resource that should be returned. + * + * @return string + * The representation of the resource. + */ + public function viewResource($resourceController, $id); + + /** + * Create a resource. + * + * @param RestWSResourceControllerInterface $resourceController + * The controller used to create the resource. + * @param string $data + * The representation of the resource. + * + * @return int|string + * The id of the newly created resource. + */ + public function createResource($resourceController, $data); + + /** + * Update a resource. + * + * @param RestWSResourceControllerInterface $resourceController + * The controller used to update the resource. + * @param int|string $id + * The id of the resource that should be updated. + * @param string $data + * The representation of the resource. + */ + public function updateResource($resourceController, $id, $data); + + /** + * Delete a resource. + * + * @param RestWSResourceControllerInterface $resourceController + * The controller used to update the resource. + * @param int|string $id + * The id of the resource that should be deleted. + */ + public function deleteResource($resourceController, $id); + + /** + * Query for a resource. + * + * If a format doesn't want to implement querying, then it should throw an + * RestWSException with the 501 HTTP status code. + * + * @param RestWSResourceControllerInterface $resourceController + * The controller used to query the resource. + * @param array $payload + * An optional way to pass the query parameters. + * + * @return string + * The serialized representation of a list of resources. + */ + public function queryResource($resourceController, $payload); + + + /** + * Returns the mime type of this format, e.g. 'application/json' or + * 'application/xml'. + */ + public function mimeType(); + + /** + * Returns the short name of this format. + * + * @return string + * The format name, example: "json". + */ + public function getName(); +} + +/** + * A base for all simple formats that are just serializing/unserializing an + * array of property values. + */ +abstract class RestWSBaseFormat implements RestWSFormatInterface { + + protected $formatName; + protected $formatInfo; + + public function __construct($name, $info) { + $this->formatName = $name; + $this->formatInfo = $info; + } + + /** + * Gets the representation of a resource. + */ + public function viewResource($resourceController, $id) { + $values = self::getData($resourceController->wrapper($id)); + $function = __FUNCTION__; + drupal_alter('restws_response', $values, $function, $this->formatName); + + return $this->serialize($values); + } + + /** + * Creates a new resource. + */ + public function createResource($resourceController, $data) { + $values = $this->unserialize($resourceController->propertyInfo(), $data); + $id = $resourceController->create($values); + $ref = self::getResourceReference($resourceController->resource(), $id); + $function = __FUNCTION__; + drupal_alter('restws_response', $ref, $function, $this->formatName); + + return $this->serialize($ref); + } + + /** + * Updates a resource. + */ + public function updateResource($resourceController, $id, $data) { + $values = $this->unserialize($resourceController->propertyInfo(), $data); + $resourceController->update($id, $values); + // Return an empty representation by default. + $value = array(); + $function = __FUNCTION__; + drupal_alter('restws_response', $value, $function, $this->formatName); + + return $this->serialize($value); + } + + /** + * Deletes a resource. + */ + public function deleteResource($resourceController, $id) { + $resourceController->delete($id); + // Return an empty representation by default. + $value = array(); + $function = __FUNCTION__; + drupal_alter('restws_response', $value, $function, $this->formatName); + + return $this->serialize($value); + } + + /** + * Implements RestWSFormatInterface::queryResource(). + */ + public function queryResource($resourceController, $payload) { + // Get the parameter from the URL. + $parameters = drupal_get_query_parameters(); + + $rest_controls = restws_meta_controls(); + $properties = $resourceController->propertyInfo(); + $split_parameters = $this->splitParameters($properties, $parameters); + + $values = $this->generateQueryURIs($resourceController, $parameters, $split_parameters['filters']); + + $full = (isset($split_parameters['meta_controls'][$rest_controls['full']])) ? $split_parameters['meta_controls'][$rest_controls['full']] : 1; + + $result = $resourceController->query($split_parameters['filters'], $split_parameters['meta_controls']); + if ($full === '0') { + foreach ($result as $id) { + $values['list'][] = $this->getResourceReference($resourceController->resource(), $id); + } + } + else { + foreach ($result as $id) { + $values['list'][] = self::getData($resourceController->wrapper($id)); + } + } + + $function = __FUNCTION__; + drupal_alter('restws_response', $values, $function, $this->formatName); + + return $this->serialize($values); + } + + public function mimeType() { + return $this->formatInfo['mime type']; + } + + public function getName() { + return $this->formatName; + } + + /** + * Gets a simple PHP array using URI references for some wrapped data. + * + * This is the counter-part of self::getPropertyValues(). + */ + public static function getData($wrapper) { + $data = array(); + $filtered = restws_property_access_filter($wrapper); + foreach ($filtered as $name => $property) { + try { + if ($property instanceof EntityDrupalWrapper) { + // For referenced entities only return the URI. + if ($id = $property->getIdentifier()) { + $data[$name] = self::getResourceReference($property->type(), $id); + } + } + elseif ($property instanceof EntityValueWrapper) { + $data[$name] = $property->value(); + } + elseif ($property instanceof EntityListWrapper || $property instanceof EntityStructureWrapper) { + $data[$name] = self::getData($property); + } + } + catch (EntityMetadataWrapperException $e) { + // A property causes problems - ignore that. + } + } + return $data; + } + + public static function getResourceReference($resource, $id) { + $return = array( + 'uri' => restws_resource_uri($resource, $id), + 'id' => $id, + 'resource' => $resource, + ); + if (module_exists('uuid') && entity_get_info($resource)) { + $ids = entity_get_uuid_by_id($resource, array($id)); + if ($id = reset($ids)) { + $return['uuid'] = $id; + } + } + return $return; + } + + /** + * Transforms simple-array data values to valid entity property values. + * + * This is the counter-part of self::getData(), thus it converts resource + * references to the required value(s). + * + * @param array $values + * The array representation of the data values. + * @param $property_info + * The property info array of the entity type for which we are transforming + * the values. + */ + protected function getPropertyValues(array &$values, $property_info) { + foreach ($values as $name => &$property_value) { + if (isset($property_info[$name]) && $info = $property_info[$name]) { + + // Check if there is a resource array and if the property has a type. + if (is_array($property_value) && isset($info['type'])) { + + // Check if the field is a list or a single value field. + if (entity_property_list_extract_type($info['type'])) { + // Check if the list values consist of structure wrappers. + if (array_key_exists('property info', $info)) { + foreach ($property_value as &$list_values) { + $this->getPropertyValues($list_values, $info['property info']); + } + } + else { + $list_type = entity_property_list_extract_type($info['type']); + foreach ($property_value as &$list_value) { + $list_value = $this->getResourceReferenceValue($list_type, $list_value); + } + } + } + else { + // Check if the property is a structure wrapper. + if (array_key_exists('property info', $info)) { + $this->getPropertyValues($property_value, $info['property info']); + } + else { + $property_value = $this->getResourceReferenceValue($info['type'], $property_value); + } + } + } + } + } + } + + /** + * Gets the resource reference value. + * + * @param $type + * The data type of the reference property. + * @param array $reference + * The input data specifying the resource reference in one supported way. + * + * @return mixed + * The value to be set for the reference. Usually this is an entity or + * resource id, but for generic entity references it's an + * EntityDrupalWrapper. + * + * @see RestWSBaseFormat::getResourceReference() + */ + protected function getResourceReferenceValue($type, array $reference) { + + if (isset($reference['id']) && $type != 'entity') { + return $reference['id']; + } + // Handle setting generic entity references, i.e. of type entity. + elseif ($type == 'entity' && isset($reference['id']) && isset($reference['resource'])) { + if (!entity_get_info($reference['resource'])) { + throw new RestWSException('Invalid resource for entity reference given.', 406); + } + return entity_metadata_wrapper($reference['resource'], $reference['id']); + } + elseif (isset($reference['uri'])) { + // @todo: Implement setting references by URI by parsing resource/id from + // the URI. + } + elseif (isset($reference['uuid']) && module_exists('uuid') && $type != 'entity') { + $ids = entity_get_id_by_uuid($type, array($reference['uuid'])); + if (!$ids) { + throw new RestWSException('Invalid UUID for resource reference given.', 406); + } + return reset($ids); + } + + throw new RestWSException('Invalid value for resource reference given.', 406); + } + + /** + * Splits a query parameter into two sub arrays containing the filters and + * meta controls. + * + * @param array $properties + * An array containing the properties of the resource. + * + * @param array $parameters + * An array which contains filters and meta controls. + * + * @return array + * An array containing two sub arrays, one for filters and one for meta + * controls with corresponding keys. + * + * @throws RestWSException + * If a filter isn't valid, the function will throw a RestWSException with + * the 412 HTTP status code. + */ + protected function splitParameters($properties, array $parameters) { + $meta_controls = array(); + $rest_controls = restws_meta_controls(); + foreach ($parameters as $control_name => $property) { + if (isset($rest_controls[$control_name])) { + $meta_controls[$control_name] = $property; + unset($parameters[$control_name]); + } + } + + $filters = array(); + foreach ($parameters as $parameter => $value) { + // Check if the property is prefixed. + if (substr($parameter, 0, 9) == 'property_') { + $parameter = substr($parameter, 9, strlen($parameter) - 9); + } + + // If the parameter doesn't exist, we can not filter for and need to + // notify the client about it. + if (!isset($properties[$parameter])) { + throw new RestWSException('Not a valid filter: ' . $parameter, 412); + } + $filters[$parameter] = $value; + } + return array('meta_controls' => $meta_controls, 'filters' => $filters); + } + + /** + * Generates all navigation links for querying. + * + * @param RestWSResourceControllerInterface $resourceController + * The controller used to query the resource. + * + * @param array $parameters + * The HTTP GET parameters for the query. + * + * @param array $filters + * The filters for the query. + * + * @return array + * An array containing all navigation links. + * + * @throws RestWSException + * If the page is out of range the function will throw a new RestWSException + * with HTTP status code 404. + */ + protected function generateQueryURIs(RestWSResourceControllerInterface $resourceController, array $parameters, array $filters) { + $rest_controls = restws_meta_controls(); + + $count = $resourceController->count($filters); + $limit = isset($parameters[$rest_controls['limit']]) ? $parameters[$rest_controls['limit']] : NULL; + $limit = $resourceController->limit($limit); + $page = isset($parameters[$rest_controls['page']]) ? $parameters[$rest_controls['page']] : 0; + + $last = ceil($count / $limit) - 1; + + if ($page > $last || $page < 0) { + throw new RestWSException('Page doesn\'t exist.', 404); + } + + $uris = array(); + $options = array( + 'query' => &$parameters, + ); + + $uris['self'] = restws_resource_uri($resourceController->resource(), null, $options); + $parameters['page'] = 0; + $uris['first'] = restws_resource_uri($resourceController->resource(), null, $options); + $parameters['page'] = $last; + $uris['last'] = restws_resource_uri($resourceController->resource(), null, $options); + + + if ($page != 0) { + $parameters['page'] = $page - 1; + $uris['prev'] = restws_resource_uri($resourceController->resource(), null, $options); + } + + if ($page != $last) { + $parameters['page'] = $page + 1; + $uris['next'] = restws_resource_uri($resourceController->resource(), null, $options); + } + + return $uris; + } +} + +/** + * Filters out properties where view access is not allowed for the current user. + * + * @param EntityMetadataWrapper $wrapper + * EntityMetadataWrapper that should be checked. + * + * @return + * An array of properties where access is allowed, keyed by their property + * name. + */ +function restws_property_access_filter($wrapper) { + $filtered = array(); + foreach ($wrapper as $name => $property) { + if ($property->access('view')) { + $filtered[$name] = $property; + } + } + return $filtered; +} + +/** + * A formatter to format json. + */ +class RestWSFormatJSON extends RestWSBaseFormat { + + public function serialize($values) { + return drupal_json_encode($values); + } + + public function unserialize($properties, $data) { + $values = drupal_json_decode($data); + $this->getPropertyValues($values, $properties); + return $values; + } +} + +/** + * A formatter for XML. + */ +class RestWSFormatXML extends RestWSBaseFormat { + + /** + * Gets the representation of a resource. + */ + public function viewResource($resourceController, $id) { + $xml = new DOMDocument('1.0', 'utf-8'); + $element = $xml->createElement($resourceController->resource()); + self::addToXML($xml, $element, $resourceController->wrapper($id)); + $xml->appendChild($element); + + $function = __FUNCTION__; + drupal_alter('restws_response', $xml, $function, $this->formatName); + + return $xml->saveXML(); + } + + /** + * Creates a new resource. + */ + public function createResource($resourceController, $data) { + $values = $this->unserialize($resourceController->propertyInfo(), $data); + $id = $resourceController->create($values); + + $xml = new DOMDocument('1.0', 'utf-8'); + $element = $xml->createElement('uri'); + self::setXMLReference($element, $resourceController->resource(), $id); + $xml->appendChild($element); + + $function = __FUNCTION__; + drupal_alter('restws_response', $xml, $function, $this->formatName); + + return $xml->saveXML(); + } + + /** + * Overrides RestWSBaseFormat::queryResource(). + */ + public function queryResource($resourceController, $payload) { + $xml = new DOMDocument('1.0', 'utf-8'); + $element = $xml->createElement('list'); + + $rest_controls = restws_meta_controls(); + $parameters = drupal_get_query_parameters(); + $properties = $resourceController->propertyInfo(); + $split_parameters = $this->splitParameters($properties, $parameters); + + $links = $this->generateQueryURIs($resourceController, $parameters, $split_parameters['filters']); + + foreach ($links as $rel => $link) { + $item = $xml->createElement('link'); + $item->setAttribute('rel', $rel); + $item->setAttribute('href', $link); + $element->appendChild($item); + } + + $full = (isset($split_parameters['meta_controls'][$rest_controls['full']])) ? $split_parameters['meta_controls'][$rest_controls['full']] : 1; + + $result = $resourceController->query($split_parameters['filters'], $split_parameters['meta_controls']); + + if ($full === '0') { + foreach ($result as $id) { + $item = $xml->createElement($resourceController->resource()); + self::setXMLReference($item, $resourceController->resource(), $id); + $element->appendChild($item); + } + } + else { + foreach ($result as $id) { + $item = $xml->createElement($resourceController->resource()); + self::addToXML($xml, $item, $resourceController->wrapper($id)); + $element->appendChild($item); + } + } + + $xml->appendChild($element); + + $function = __FUNCTION__; + drupal_alter('restws_response', $xml, $function, $this->formatName); + + return $xml->saveXML(); + } + + public function serialize($data) { + // Return an empty XML document. + $xml = new DOMDocument('1.0', 'utf-8'); + return $xml->saveXML(); + } + + public function unserialize($properties, $data) { + $xml = simplexml_load_string($data); + return $this->xmlToArray($properties, $xml); + } + + /** + * Turns the xml structure into an array of values. + */ + public function xmlToArray($properties, SimpleXMLElement $xml, $listItemType = NULL) { + foreach ($xml->children() as $name => $element) { + // Check if we are processing an entity, an item from a list or a list. + if ((isset($properties[$name]['type']) && (entity_property_list_extract_type($properties[$name]['type']) || entity_get_info($properties[$name]['type']))) || isset($listItemType)) { + // If we are processing a list, then set the type of the list and save + // the results into a a numeric array. + if (isset($listItemType)) { + $type = $listItemType; + $result_pointer = &$result[]; + } + else { + $type = $properties[$name]['type']; + $result_pointer = &$result[$name]; + } + + // Check if the type is a list. + if (entity_property_list_extract_type($type)) { + $result_pointer = $this->xmlToArray($properties, $element, entity_property_list_extract_type($type)); + } + else { + $attributes = $element->attributes(); + $values['id'] = (string)$attributes['id']; + $values['resource'] = (string)$attributes['resource']; + $values['uri'] = $this->xmlToArray($properties, $element); + $id = $this->getResourceReferenceValue($type, $values); + // If an id could be extracted, then a resource array was send. + if ($id !== FALSE) { + $result_pointer = $id; + } + else { + // If no ID could be extracted, then save the inner text content of + // the node, which is saved in the $values['uri']. + $result_pointer = $values['uri']; + } + } + } + else { + $result[$name] = $this->xmlToArray($properties, $element); + } + foreach ($xml->attributes() as $attribute_name => $attribute_value) { + $result[$attribute_name] = $attribute_value; + } + } + if (!isset($result)) { + $result = ($string = (string) $xml) ? $string : NULL; + } + return $result; + } + + /** + * Adds the data of the given wrapper to the given XML element. + */ + public static function addToXML(DOMDocument $doc, DOMNode $parent, $wrapper) { + $filtered = restws_property_access_filter($wrapper); + foreach ($filtered as $name => $property) { + try { + if ($property instanceof EntityDrupalWrapper) { + // For referenced entities only return the URI. + if ($id = $property->getIdentifier()) { + $element = $doc->createElement(is_numeric($name) ? 'item' : $name); + $parent->appendChild($element); + self::setXMLReference($element, $property->type(), $id); + } + } + elseif ($property instanceof EntityValueWrapper) { + // Only primitive data types are allowed here. There might be complex + // arrays/objects in EntityValueWrapper if no property information is + // provided (example: the "data" property of commerce_price fields. + if (is_scalar($property->value())) { + $escaped = $doc->createTextNode($property->value()); + $element = $doc->createElement(is_numeric($name) ? 'item' : $name); + $element->appendChild($escaped); + $parent->appendChild($element); + } + } + elseif ($property instanceof EntityListWrapper || $property instanceof EntityStructureWrapper) { + $element = $doc->createElement(is_numeric($name) ? 'item' : $name); + $parent->appendChild($element); + self::addToXML($doc, $element, $property); + } + } + catch (EntityMetadataWrapperException $e) { + // A property causes problems - ignore that. + } + } + } + + public static function setXMLReference(DOMElement $node, $resource, $id) { + $node->nodeValue = restws_resource_uri($resource, $id); + $node->setAttribute('resource', $resource); + $node->setAttribute('id', $id); + } +} + +/** + * A simple formatter for RDF. Requires the RDF module for the mapping. + */ +class RestWSFormatRDF extends RestWSBaseFormat { + + protected $namespaces; + + public function __construct($name, $info) { + parent::__construct($name, $info); + $this->namespaces = rdf_get_namespaces(); + $this->namespaces['rdf'] = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + } + + /** + * Gets the representation of a resource. + */ + public function viewResource($resourceController, $id) { + $xml = new DOMDocument('1.0', 'utf-8'); + $rdf_element = $xml->createElementNS($this->namespaces['rdf'], 'rdf:RDF'); + $xml->appendChild($rdf_element); + + $element = $xml->createElementNS($this->namespaces['rdf'], 'rdf:Description'); + $element->setAttributeNS($this->namespaces['rdf'], 'rdf:about', restws_resource_uri($resourceController->resource(), $id)); + + // Add the RDF type of the resource if there is a mapping. + $entity = $resourceController->read($id); + if (!empty($entity->rdf_mapping['rdftype'])) { + foreach ($entity->rdf_mapping['rdftype'] as $rdf_type) { + $type_element = $xml->createElementNS($this->namespaces['rdf'], 'rdf:type'); + list($ns, $name) = explode(':', $rdf_type); + $type_element->setAttributeNS($this->namespaces['rdf'], 'rdf:resource', $this->namespaces[$ns] . $name); + $element->appendChild($type_element); + } + } + + $this->addToXML($xml, $element, $resourceController->wrapper($id)); + $rdf_element->appendChild($element); + + $function = __FUNCTION__; + drupal_alter('restws_response', $xml, $function, $this->formatName); + + return $xml->saveXML(); + } + + public function createResource($resourceController, $data) { + throw new RestWSException('Not implemented', 501); + } + + public function updateResource($resourceController, $id, $data) { + throw new RestWSException('Not implemented', 501); + } + + public function queryResource($resourceController, $parameters) { + throw new RestWSException('Not implemented', 501); + } + + /** + * Adds the data of the given wrapper to the given XML element. + */ + public function addToXML(DOMDocument $doc, DOMNode $parent, $wrapper) { + $filtered = restws_property_access_filter($wrapper); + foreach ($filtered as $name => $property) { + try { + if ($property instanceof EntityDrupalWrapper) { + // For referenced entities only return the URI. + if ($id = $property->getIdentifier()) { + $element = $this->addRdfElement($doc, $wrapper, $name); + $parent->appendChild($element); + $this->addReference($doc, $element, $property->type(), $id); + } + } + elseif ($property instanceof EntityValueWrapper) { + $element = $this->addRdfElement($doc, $wrapper, $name); + $parent->appendChild($element); + $element->nodeValue = $property->value(); + } + elseif ($property instanceof EntityListWrapper || $property instanceof EntityStructureWrapper) { + $element = $this->addRdfElement($doc, $wrapper, $name); + $parent->appendChild($element); + $node = $doc->createElementNS($this->namespaces['rdf'], 'rdf:Description'); + $element->appendChild($node); + $this->addToXML($doc, $node, $property); + } + } + catch (EntityMetadataWrapperException $e) { + // A property causes problems - ignore that. + } + } + } + + public function addReference(DomDocument $doc, DOMElement $node, $resource, $id) { + $element = $doc->createElementNS($this->namespaces['rdf'], 'rdf:Description'); + $element->setAttributeNS($this->namespaces['rdf'], 'rdf:about', restws_resource_uri($resource, $id)); + $node->appendChild($element); + } + + /** + * Adds an RDF element for the given property of the wrapper using the RDF + * mapping. + */ + public function addRdfElement(DOMDOcument $doc, EntityMetadataWrapper $wrapper, $name) { + if ($wrapper instanceof EntityDrupalWrapper) { + $entity = $wrapper->value(); + if (!empty($entity->rdf_mapping[$name])) { + // Just make use of the first predicate for now. + $predicate = reset($entity->rdf_mapping[$name]['predicates']); + list($ns, $qname) = explode(':', $predicate); + $element = $doc->createElementNS($this->namespaces[$ns], $predicate); + + if (!empty($entity->rdf_mapping[$name]['datatype'])) { + $element->setAttributeNS($this->namespaces['rdf'], 'rdf:datatype', $entity->rdf_mapping[$name]['datatype']); + } + } + } + if (!isset($element)) { + // For other elements just use the site URL as namespace. + $element = $doc->createElementNS(url('', array('absolute' => TRUE)), 'site:' . (is_numeric($name) ? 'item' : $name)); + } + return $element; + } +}