diff --git a/core/modules/hal/lib/Drupal/hal/Encoder/JsonEncoder.php b/core/modules/hal/lib/Drupal/hal/Encoder/JsonEncoder.php index 6d1da5d..4d932ff 100644 --- a/core/modules/hal/lib/Drupal/hal/Encoder/JsonEncoder.php +++ b/core/modules/hal/lib/Drupal/hal/Encoder/JsonEncoder.php @@ -30,4 +30,11 @@ public function supportsEncoding($format) { return $format == $this->format; } + /** + * Overrides \Symfony\Component\Serializer\Encoder\JsonEncoder::supportsDecoding() + */ + public function supportsDecoding($format) { + return $format == $this->format; + } + } diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php index 0613d2a..af109a3 100644 --- a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityNG; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** * Converts the Drupal entity object structure to a HAL array structure. @@ -62,6 +63,63 @@ public function normalize($entity, $format = NULL, array $context = array()) { } /** + * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize(). + * + * @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException + */ + public function denormalize($data, $class, $format = NULL, array $context = array()) { + // Get type, necessary for determining which bundle to create. + if (!isset($data['_links']['type'])) { + throw new UnexpectedValueException('The type link relation must be specified.'); + } + $type_uri = $data['_links']['type']['href']; + + // Get language. language_default() is used because otherwise the entity + // system cannot understand the langcode when working in the DrupalUnitTest + // environment and importing data without an explicit langcode. + $langcode = isset($data['langcode']) ? $data['langcode'][0]['value'] : language_default(); + + // Create the entity. + if ($typed_data_ids = $this->linkManager->getTypeInternalIds($type_uri)) { + $entity = entity_create($typed_data_ids['entity_type'], array('langcode' => $langcode, 'type' => $typed_data_ids['bundle'])); + } + else { + throw new UnexpectedValueException(sprintf('Type %s does not correspond to an entity on this site.', $type_uri)); + } + + // Get links and remove from data array. + $links = $data['_links']; + unset($data['_links']); + // Get embedded resources and remove from data array. + $embedded = array(); + if (isset($data['_embedded'])) { + $embedded = $data['_embedded']; + unset($data['_embedded']); + } + + // Iterate through remaining items in data array. These should all + // correspond to fields. + foreach ($data as $field_name => $field_data) { + // Remove any values that were set as a part of entity creation (e.g + // uuid). If this field is set to an empty array in the data, this will + // also have the effect of marking the field for deletion in REST module. + $entity->{$field_name} = array(); + + $field = $entity->get($field_name); + // Get the class of the field. This will generally be the default Field + // class. + $class = get_class($field); + // Pass in the empty field object as a target instance. Since the context + // is already prepared for the field, any data added to it is + // automatically added to the entity. + $context['target_instance'] = $field; + $this->serializer->denormalize($field_data, $class, $format, $context); + } + + return $entity; + } + + /** * Constructs the entity URI. * * @param $entity diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php index b56c2bb..76c43c7 100644 --- a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php @@ -57,4 +57,11 @@ public function normalize($field_item, $format = NULL, array $context = array()) ); } + /** + * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize() + */ + public function denormalize($data, $class, $format = NULL, array $context = array()) { + // @todo Implement this in http://drupal.org/node/1880424 + } + } diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php index b06a90f..2d3e967 100644 --- a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php @@ -7,6 +7,8 @@ namespace Drupal\hal\Normalizer; +use Drupal\Core\Entity\Field\FieldItemInterface; + /** * Converts the Drupal field item object structure to HAL array structure. */ @@ -38,4 +40,72 @@ public function normalize($field_item, $format = NULL, array $context = array()) ); } + /** + * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize() + */ + public function denormalize($data, $class, $format = NULL, array $context = array()) { + if (!isset($context['target_instance'])) { + throw new LogicException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer'); + } + if ($context['target_instance']->getParent() == NULL) { + throw new LogicException('The field item passed in via $context[\'target_instance\'] must have a parent set.'); + } + + $field_item = $context['target_instance']; + + // If this field is translatable, we need to create a translated instance. + if (isset($data['lang'])) { + $langcode = $data['lang']; + unset($data['lang']); + $field_definition = $field_item->getDefinition(); + if ($field_definition['translatable'] == TRUE) { + $field_item = $this->createTranslatedInstance($field_item, $langcode); + } + } + + $field_item->setValue($data); + return $field_item; + } + + /** + * Get a translated version of the field item instance. + * + * To indicate that a field item applies to one translation of an entity and + * not another, the property path must originate with a translation of the + * entity. This is the reason for using target_instances, from which the + * property path can be traversed up to the root. + * + * @param \Drupal\Core\Entity\Field\FieldItemInterface $field_item + * The untranslated field item instance. + * @param $langcode + * The langcode. + * + * @return \Drupal\Core\Entity\Field\FieldItemInterface + * The translated field item instance. + */ + protected function createTranslatedInstance(FieldItemInterface $field_item, $langcode) { + $parent = $field_item->getParent(); + $ancestors = array(); + + // Remove the untranslated instance from the field's list of items. + $parent->offsetUnset($field_item->getName()); + + // Get the property path. + while (!method_exists($parent, 'getTranslation')) { + array_unshift($ancestors, $parent); + $parent = $parent->getParent(); + } + + // Recreate the property path with translations. + $translation = $parent->getTranslation($langcode); + foreach ($ancestors as $ancestor) { + $ancestor_name = $ancestor->getName(); + $translation = $translation->get($ancestor_name); + } + + // Create a new instance at the end of the property path and return it. + $count = $translation->isEmpty() ? 0 : $translation->count(); + return $translation->offsetGet($count); + } + } diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php index d088bca..d9f40a8 100644 --- a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php @@ -8,6 +8,7 @@ namespace Drupal\hal\Normalizer; use Drupal\Component\Utility\NestedArray; +use Symfony\Component\Serializer\Exception\LogicException; /** * Converts the Drupal field structure to HAL array structure. @@ -26,6 +27,8 @@ class FieldNormalizer extends NormalizerBase { */ public function normalize($field, $format = NULL, array $context = array()) { $normalized_field_items = array(); + + // Get the field definition. $entity = $field->getParent(); $field_name = $field->getName(); $field_definition = $entity->getPropertyDefinition($field_name); @@ -53,6 +56,34 @@ public function normalize($field, $format = NULL, array $context = array()) { return $normalized; } + + /** + * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize() + */ + public function denormalize($data, $class, $format = NULL, array $context = array()) { + if (!isset($context['target_instance'])) { + throw new LogicException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer'); + } + if ($context['target_instance']->getParent() == NULL) { + throw new LogicException('The field passed in via $context[\'target_instance\'] must have a parent set.'); + } + + $field = $context['target_instance']; + foreach ($data as $field_item_data) { + $count = $field->isEmpty() ? 0 : $field->count(); + // Get the next field item instance. The offset will serve as the field + // item name. + $field_item = $field->offsetGet($count); + $field_item_class = get_class($field_item); + // Pass in the empty field item object as the target instance. + $context['target_instance'] = $field_item; + $this->serializer->denormalize($field_item_data, $field_item_class, $format, $context); + } + + return $field; + + } + /** * Helper function to normalize field items. * diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php index d159f1f..afa9019 100644 --- a/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php @@ -8,11 +8,12 @@ namespace Drupal\hal\Normalizer; use Drupal\serialization\Normalizer\NormalizerBase as SerializationNormalizerBase; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; /** * Base class for Normalizers. */ -abstract class NormalizerBase extends SerializationNormalizerBase { +abstract class NormalizerBase extends SerializationNormalizerBase implements DenormalizerInterface { /** * The formats that the Normalizer can handle. @@ -36,6 +37,22 @@ public function supportsNormalization($data, $format = NULL) { } /** + * Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::supportsDenormalization() + */ + public function supportsDenormalization($data, $type, $format = NULL) { + if (in_array($format, $this->formats)) { + $target = new \ReflectionClass($type); + $supported = new \ReflectionClass($this->supportedInterfaceOrClass); + if ($supported->isInterface()) { + return $target->implementsInterface($this->supportedInterfaceOrClass); + } + else { + return ($target->getName() == $this->supportedInterfaceOrClass || $target->isSubclassOf($this->supportedInterfaceOrClass)); + } + } + } + + /** * Sets the link manager. * * The link manager determines the hypermedia type and relation links which diff --git a/core/modules/hal/lib/Drupal/hal/Tests/DenormalizeTest.php b/core/modules/hal/lib/Drupal/hal/Tests/DenormalizeTest.php new file mode 100644 index 0000000..bf15e87 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Tests/DenormalizeTest.php @@ -0,0 +1,169 @@ + 'Denormalize Test', + 'description' => 'Test that entities can be denormalized from HAL.', + 'group' => 'HAL', + ); + } + + /** + * Tests that the type link relation in incoming data is handled correctly. + */ + public function testTypeHandling() { + // Valid type. + $data_with_valid_type = array( + '_links' => array( + 'type' => array( + 'href' => url('rest/type/entity_test/entity_test', array('absolute' => TRUE)), + ), + ), + ); + $denormalized = $this->container->get('serializer')->denormalize($data_with_valid_type, $this->entityClass, $this->format); + $this->assertEqual(get_class($denormalized), $this->entityClass, 'Request with valid type results in creation of correct bundle.'); + + // Invalid type. + $data_with_invalid_type = array( + '_links' => array( + 'type' => array( + 'href' => url('rest/types/foo', array('absolute' => TRUE)), + ), + ), + ); + try { + $this->container->get('serializer')->denormalize($data_with_invalid_type, $this->entityClass, $this->format); + $this->fail('Exception thrown when type is invalid.'); + } + catch (UnexpectedValueException $e) { + $this->pass('Exception thrown when type is invalid.'); + } + + // No type. + $data_with_no_type = array( + '_links' => array( + ), + ); + try { + $this->container->get('serializer')->denormalize($data_with_no_type, $this->entityClass, $this->format); + $this->fail('Exception thrown when no type is provided.'); + } + catch (UnexpectedValueException $e) { + $this->pass('Exception thrown when no type is provided.'); + } + } + + /** + * Test that a field set to an empty array is different than an empty field. + */ + public function testMarkFieldForDeletion() { + $no_field_data = array( + '_links' => array( + 'type' => array( + 'href' => url('rest/type/entity_test/entity_test', array('absolute' => TRUE)), + ), + ), + ); + $no_field_denormalized = $this->container->get('serializer')->denormalize($no_field_data, $this->entityClass, $this->format); + $no_field_value = $no_field_denormalized->field_test_text->getValue(); + + $empty_field_data = array( + '_links' => array( + 'type' => array( + 'href' => url('rest/type/entity_test/entity_test', array('absolute' => TRUE)), + ), + ), + 'field_test_text' => array(), + ); + $empty_field_denormalized = $this->container->get('serializer')->denormalize($empty_field_data, $this->entityClass, $this->format); + $empty_field_value = $empty_field_denormalized->field_test_text->getValue(); + + $this->assertTrue(!empty($no_field_value) && empty($empty_field_value), 'A field set to an empty array in the data is structured differently than an empty field.'); + } + + /** + * Test that non-reference fields can be denormalized. + */ + public function testBasicFieldDenormalization() { + $data = array( + '_links' => array( + 'type' => array( + 'href' => url('rest/type/entity_test/entity_test', array('absolute' => TRUE)), + ), + ), + 'uuid' => array( + array( + 'value' => 'e5c9fb96-3acf-4a8d-9417-23de1b6c3311', + ), + ), + 'field_test_text' => array( + array( + 'value' => $this->randomName(), + 'format' => 'full_html', + ), + ), + 'field_test_translatable_text' => array( + array( + 'value' => $this->randomName(), + 'format' => 'full_html', + 'lang' => 'en', + ), + array( + 'value' => $this->randomName(), + 'format' => 'filtered_html', + 'lang' => 'en', + ), + array( + 'value' => $this->randomName(), + 'format' => 'filtered_html', + 'lang' => 'de', + ), + array( + 'value' => $this->randomName(), + 'format' => 'full_html', + 'lang' => 'de', + ), + ), + ); + + $expected_value_en = array( + array ( + 'value' => $data['field_test_translatable_text'][0]['value'], + 'format' => 'full_html', + ), + array ( + 'value' => $data['field_test_translatable_text'][1]['value'], + 'format' => 'filtered_html', + ), + ); + $expected_value_de = array( + array ( + 'value' => $data['field_test_translatable_text'][2]['value'], + 'format' => 'filtered_html', + ), + array ( + 'value' => $data['field_test_translatable_text'][3]['value'], + 'format' => 'full_html', + ), + ); + $denormalized = $this->container->get('serializer')->denormalize($data, $this->entityClass, $this->format); + $this->assertEqual($data['uuid'], $denormalized->get('uuid')->getValue(), 'A preset value (e.g. UUID) is overridden by incoming data.'); + $this->assertEqual($data['field_test_text'], $denormalized->get('field_test_text')->getValue(), 'A basic text field is denormalized.'); + $this->assertEqual($expected_value_en, $denormalized->get('field_test_translatable_text')->getValue(), 'Values in the default language are properly handled for a translatable field.'); + $this->assertEqual($expected_value_de, $denormalized->getTranslation('de')->get('field_test_translatable_text')->getValue(), 'Values in a translation language are properly handled for a translatable field.'); + } +} diff --git a/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php index 224586d..9cf7019 100644 --- a/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php +++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php @@ -30,6 +30,13 @@ protected $format = 'hal_json'; /** + * The class name of the test class. + * + * @var string + */ + protected $entityClass = 'Drupal\entity_test\Plugin\Core\Entity\EntityTest'; + + /** * Overrides \Drupal\simpletest\DrupalUnitTestBase::setup(). */ function setUp() { @@ -57,7 +64,6 @@ function setUp() { $field = array( 'field_name' => 'field_test_text', 'type' => 'text', - 'cardinality' => 1, 'translatable' => FALSE, ); field_create_field($field); @@ -68,6 +74,20 @@ function setUp() { ); field_create_instance($instance); + // Create the test translatable field. + $field = array( + 'field_name' => 'field_test_translatable_text', + 'type' => 'text', + 'translatable' => TRUE, + ); + field_create_field($field); + $instance = array( + 'entity_type' => 'entity_test', + 'field_name' => 'field_test_translatable_text', + 'bundle' => 'entity_test', + ); + field_create_instance($instance); + // Create the test entity reference field. $field = array( 'translatable' => TRUE, diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php index 3f5b947..2004a6c 100644 --- a/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php @@ -43,6 +43,13 @@ public function getTypeUri($entity_type, $bundle) { } /** + * Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeInternalIds(). + */ + public function getTypeInternalIds($type_uri) { + return $this->typeLinkManager->getTypeInternalIds($type_uri); + } + + /** * Implements \Drupal\rest\LinkManager\RelationLinkManagerInterface::getRelationUri(). */ public function getRelationUri($entity_type, $bundle, $field_name) { diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManager.php b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManager.php index 7988bfe..6802282 100644 --- a/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManager.php +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManager.php @@ -7,9 +7,28 @@ namespace Drupal\rest\LinkManager; +use Drupal\Core\Cache\CacheBackendInterface; + class TypeLinkManager implements TypeLinkManagerInterface { /** + * Injected cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface; + */ + protected $cache; + + /** + * Constructor. + * + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The injected cache backend for caching type URIs. + */ + public function __construct(CacheBackendInterface $cache) { + $this->cache = $cache; + } + + /** * Get a type link for a bundle. * * @param string $entity_type @@ -25,4 +44,62 @@ public function getTypeUri($entity_type, $bundle) { return url("rest/type/$entity_type/$bundle", array('absolute' => TRUE)); } + /** + * Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeInternalIds(). + */ + public function getTypeInternalIds($type_uri) { + $types = $this->getTypes(); + if (isset($types[$type_uri])) { + return $types[$type_uri]; + } + return FALSE; + } + + /** + * Get the array of type links. + * + * @return array + * An array of typed data ids (entity_type and bundle) keyed by + * corresponding type URI. + */ + public function getTypes() { + $cid = 'rest:links:types'; + $cache = $this->cache->get($cid); + if (!$cache) { + $this->writeCache(); + $cache = $this->cache->get($cid); + } + return $cache->data; + } + + /** + * Writes the cache of type links. + */ + protected function writeCache() { + $data = array(); + + // Type URIs correspond to bundles. Iterate through the bundles to get the + // URI and data for them. + $entity_info = entity_get_info(); + foreach (entity_get_bundles() as $entity_type => $bundles) { + $entity_type_info = $entity_info[$entity_type]; + $reflection = new \ReflectionClass($entity_type_info['class']); + // Only content entities are supported currently. + // @todo Consider supporting config entities. + if ($reflection->implementsInterface('\Drupal\Core\Config\Entity\ConfigEntityInterface')) { + continue; + } + foreach ($bundles as $bundle => $bundle_info) { + // Get a type URI for the bundle. + $bundle_uri = $this->getTypeUri($entity_type, $bundle); + $data[$bundle_uri] = array( + 'entity_type' => $entity_type, + 'bundle' => $bundle, + ); + } + } + // These URIs only change when entity info changes, so cache it permanently + // and only clear it when entity_info is cleared. + $this->cache->set('rest:links:types', $data, CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE)); + } } diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php index ba4dc3c..9c2d942 100644 --- a/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php @@ -25,4 +25,16 @@ * The corresponding URI for the bundle. */ public function getTypeUri($entity_type, $bundle); + + /** + * Get a bundle's Typed Data IDs based on a URI. + * + * @param string $type_uri + * The type URI. + * + * @return array | boolean + * If the URI matches a bundle, returns an array containing entity_type and + * bundle. Otherwise, returns false. + */ + public function getTypeInternalIds($type_uri); } diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php index b582a9f..5f910cb 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php @@ -155,6 +155,11 @@ public function patch($id, EntityInterface $entity) { } // Overwrite the received properties. foreach ($entity->getProperties() as $name => $property) { + // Requests cannot overwrite id or uuid, so skip them. + if (in_array($name, array('id', 'uuid'))) { + continue; + } + if (isset($entity->{$name})) { $original_entity->{$name} = $property; } diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php index 30156de..e0d09bc 100644 --- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php +++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php @@ -75,7 +75,7 @@ public function handle(Request $request, $id = NULL) { } catch (HttpException $e) { $error['error'] = $e->getMessage(); - $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld'; + $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'hal_json'; $content = $serializer->serialize($error, $format); // Add the default content type, but only if the headers from the // exception have not specified it already. @@ -88,9 +88,8 @@ public function handle(Request $request, $id = NULL) { if ($data != NULL) { // All REST routes are restricted to exactly one format, so instead of // parsing it out of the Accept headers again we can simply retrieve the - // format requirement. If there is no format associated just pick Drupal - // JSON-LD. - $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'drupal_jsonld'; + // format requirement. If there is no format associated just pick HAL. + $format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'hal_json'; $output = $serializer->serialize($data, $format); $response->setContent($output); diff --git a/core/modules/rest/lib/Drupal/rest/RestBundle.php b/core/modules/rest/lib/Drupal/rest/RestBundle.php index 37db78e..1c98b79 100644 --- a/core/modules/rest/lib/Drupal/rest/RestBundle.php +++ b/core/modules/rest/lib/Drupal/rest/RestBundle.php @@ -36,7 +36,8 @@ public function build(ContainerBuilder $container) { $container->register('rest.link_manager', 'Drupal\rest\LinkManager\LinkManager') ->addArgument(new Reference('rest.link_manager.type')) ->addArgument(new Reference('rest.link_manager.relation')); - $container->register('rest.link_manager.type', 'Drupal\rest\LinkManager\TypeLinkManager'); + $container->register('rest.link_manager.type', 'Drupal\rest\LinkManager\TypeLinkManager') + ->addArgument(new Reference('cache.cache')); $container->register('rest.link_manager.relation', 'Drupal\rest\LinkManager\RelationLinkManager'); } } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php index d6973e5..e505a07 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php @@ -19,7 +19,7 @@ class CreateTest extends RESTTestBase { * * @var array */ - public static $modules = array('rest', 'entity_test'); + public static $modules = array('hal', 'rest', 'entity_test'); public static function getInfo() { return array( @@ -46,9 +46,9 @@ public function testCreate() { $entity_values = $this->entityValues($entity_type); $entity = entity_create($entity_type, $entity_values); - $serialized = $serializer->serialize($entity, 'drupal_jsonld'); + $serialized = $serializer->serialize($entity, 'hal_json'); // Create the entity over the REST API. - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/hal+json'); $this->assertResponse(201); // Get the new entity ID from the location header and try to read it from @@ -71,7 +71,7 @@ public function testCreate() { $loaded_entity->delete(); // Try to send invalid data that cannot be correctly deserialized. - $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', 'application/hal+json'); $this->assertResponse(400); // Try to create an entity without the CSRF token. @@ -82,21 +82,21 @@ public function testCreate() { CURLOPT_POSTFIELDS => $serialized, CURLOPT_URL => url('entity/' . $entity_type, array('absolute' => TRUE)), CURLOPT_NOBODY => FALSE, - CURLOPT_HTTPHEADER => array('Content-Type: application/vnd.drupal.ld+json'), + CURLOPT_HTTPHEADER => array('Content-Type: application/hal+json'), )); $this->assertResponse(403); $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.'); // Try to create an entity without proper permissions. $this->drupalLogout(); - $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/hal+json'); $this->assertResponse(403); $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.'); // Try to create a resource which is not REST API enabled. $this->enableService(FALSE); $this->drupalLogin($account); - $this->httpRequest('entity/entity_test', 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/entity_test', 'POST', $serialized, 'application/hal+json'); $this->assertResponse(404); $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.'); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php index 9be9896..f5910a3 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php @@ -19,7 +19,7 @@ class DBLogTest extends RESTTestBase { * * @var array */ - public static $modules = array('jsonld', 'rest', 'dblog'); + public static $modules = array('hal', 'rest', 'dblog'); public static function getInfo() { return array( @@ -50,16 +50,16 @@ public function testWatchdog() { $account = $this->drupalCreateUser(array('restful get dblog')); $this->drupalLogin($account); - $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/vnd.drupal.ld+json'); + $response = $this->httpRequest("dblog/$id", 'GET', NULL, 'application/hal+json'); $this->assertResponse(200); - $this->assertHeader('content-type', 'application/vnd.drupal.ld+json'); + $this->assertHeader('content-type', 'application/hal+json'); $log = drupal_json_decode($response); $this->assertEqual($log['wid'], $id, 'Log ID is correct.'); $this->assertEqual($log['type'], 'rest_test', 'Type of log message is correct.'); $this->assertEqual($log['message'], 'Test message', 'Log message text is correct.'); // Request an unknown log entry. - $response = $this->httpRequest("dblog/9999", 'GET', NULL, 'application/vnd.drupal.ld+json'); + $response = $this->httpRequest("dblog/9999", 'GET', NULL, 'application/hal+json'); $this->assertResponse(404); $decoded = drupal_json_decode($response); $this->assertEqual($decoded['error'], 'Log entry with ID 9999 was not found', 'Response message is correct.'); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php index 2bed8ba..7667365 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php @@ -19,7 +19,7 @@ class DeleteTest extends RESTTestBase { * * @var array */ - public static $modules = array('rest', 'entity_test'); + public static $modules = array('hal', 'rest', 'entity_test'); public static function getInfo() { return array( diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index 6cbfbd8..9a62b1e 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -26,7 +26,7 @@ * @param string $format * The MIME type of the transmitted content. */ - protected function httpRequest($url, $method, $body = NULL, $format = 'application/ld+json') { + protected function httpRequest($url, $method, $body = NULL, $format = 'application/hal+json') { if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) { // GET the CSRF token first for writing requests. $token = $this->drupalGet('rest/session/token'); @@ -159,7 +159,7 @@ protected function entityValues($entity_type) { * @param string $method * The HTTP method to enable, e.g. GET, POST etc. * @param string $format - * (Optional) The serialization format, e.g. jsonld. + * (Optional) The serialization format, e.g. hal_json. */ protected function enableService($resource_type, $method = 'GET', $format = NULL) { // Enable REST API for this entity type. diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php index e157a2d..2b77bbd 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php @@ -19,7 +19,7 @@ class ReadTest extends RESTTestBase { * * @var array */ - public static $modules = array('jsonld', 'rest', 'entity_test'); + public static $modules = array('hal', 'rest', 'entity_test'); public static function getInfo() { return array( @@ -48,34 +48,34 @@ public function testRead() { $entity = $this->entityCreate($entity_type); $entity->save(); // Read it over the REST API. - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json'); + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/hal+json'); $this->assertResponse('200', 'HTTP response code is correct.'); - $this->assertHeader('content-type', 'application/vnd.drupal.ld+json'); + $this->assertHeader('content-type', 'application/hal+json'); $data = drupal_json_decode($response); // Only assert one example property here, other properties should be // checked in serialization tests. - $this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct'); + $this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct'); // Try to read the entity with an unsupported mime format. $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat'); $this->assertResponse(415); // Try to read an entity that does not exist. - $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/vnd.drupal.ld+json'); + $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/hal+json'); $this->assertResponse(404); $decoded = drupal_json_decode($response); $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.'); // Try to read an entity without proper permissions. $this->drupalLogout(); - $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json'); + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/hal+json'); $this->assertResponse(403); $this->assertNull(drupal_json_decode($response), 'No valid JSON found.'); } // Try to read a resource which is not REST API enabled. $account = $this->drupalCreateUser(); $this->drupalLogin($account); - $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/vnd.drupal.ld+json'); + $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/hal+json'); $this->assertResponse(404); $this->assertNull(drupal_json_decode($response), 'No valid JSON found.'); } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php index c920514..1cf36d3 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php @@ -19,7 +19,7 @@ class UpdateTest extends RESTTestBase { * * @var array */ - public static $modules = array('rest', 'entity_test'); + public static $modules = array('hal', 'rest', 'entity_test'); public static function getInfo() { return array( @@ -51,12 +51,10 @@ public function testPatchUpdate() { // Create a second stub entity for overwriting a field. $patch_values['field_test_text'] = array(0 => array('value' => $this->randomString())); $patch_entity = entity_create($entity_type, $patch_values); - // We don't want to overwrite the UUID. - unset($patch_entity->uuid); - $serialized = $serializer->serialize($patch_entity, 'drupal_jsonld'); + $serialized = $serializer->serialize($patch_entity, 'hal_json'); // Update the entity over the REST API. - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json'); $this->assertResponse(204); // Re-load updated entity from the database. @@ -64,12 +62,12 @@ public function testPatchUpdate() { $this->assertEqual($entity->field_test_text->value, $patch_entity->field_test_text->value, 'Field was successfully updated.'); // Try to empty a field. - $normalized = $serializer->normalize($patch_entity, 'drupal_jsonld'); + $normalized = $serializer->normalize($patch_entity, 'hal_json'); $normalized['field_test_text'] = array(); - $serialized = $serializer->encode($normalized, 'jsonld'); + $serialized = $serializer->encode($normalized, 'hal_json'); // Update the entity over the REST API. - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json'); $this->assertResponse(204); // Re-load updated entity from the database. @@ -77,20 +75,20 @@ public function testPatchUpdate() { $this->assertNull($entity->field_test_text->value, 'Test field has been cleared.'); // Try to update a non-existing entity with ID 9999. - $this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, 'application/hal+json'); $this->assertResponse(404); $loaded_entity = entity_load($entity_type, 9999, TRUE); $this->assertFalse($loaded_entity, 'Entity 9999 was not created.'); // Try to update an entity without proper permissions. $this->drupalLogout(); - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json'); $this->assertResponse(403); // Try to update a resource which is not REST API enabled. $this->enableService(FALSE); $this->drupalLogin($account); - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/hal+json'); $this->assertResponse(404); } @@ -120,9 +118,9 @@ public function testPutUpdate() { $update_entity->uuid->value = $entity->uuid(); $update_entity->id->value = $entity->id(); - $serialized = $serializer->serialize($update_entity, 'drupal_jsonld'); + $serialized = $serializer->serialize($update_entity, 'hal_json'); // Update the entity over the REST API. - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json'); $this->assertResponse(204); // Re-load the updated entity from the database. @@ -138,8 +136,8 @@ public function testPutUpdate() { // Try to delete a property. unset($update_entity->field_test_text); - $serialized = $serializer->serialize($update_entity, 'drupal_jsonld'); - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $serialized = $serializer->serialize($update_entity, 'hal_json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json'); $this->assertResponse(204); // Re-load the updated entity from the database. @@ -147,20 +145,20 @@ public function testPutUpdate() { $this->assertTrue($entity->field_test_text->isEmpty(), 'Property has been deleted.'); // Try to create an entity with ID 9999. - $this->httpRequest('entity/' . $entity_type . '/9999', 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/9999', 'PUT', $serialized, 'application/hal+json'); $this->assertResponse(404); $loaded_entity = entity_load($entity_type, 9999, TRUE); $this->assertFalse($loaded_entity, 'Entity 9999 was not created.'); // Try to update an entity without proper permissions. $this->drupalLogout(); - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json'); $this->assertResponse(403); // Try to update a resource which is not REST API enabled. $this->enableService(FALSE); $this->drupalLogin($account); - $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/vnd.drupal.ld+json'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PUT', $serialized, 'application/hal+json'); $this->assertResponse(404); } } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php b/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php index 4aeaaa7..c38db82 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/Views/StyleSerializerTest.php @@ -25,7 +25,7 @@ class StyleSerializerTest extends PluginTestBase { * * @var array */ - public static $modules = array('views_ui', 'entity_test', 'jsonld', 'rest_test_views'); + public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views'); /** * Views used by this test. @@ -127,13 +127,9 @@ public function testSerializerResponses() { $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.'); - $expected = $serializer->serialize($entities, 'jsonld'); - $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/ld+json')); - $this->assertIdentical($actual_json, $expected, 'The expected JSONLD output was found.'); - - $expected = $serializer->serialize($entities, 'drupal_jsonld'); - $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/vnd.drupal.ld+json')); - $this->assertIdentical($actual_json, $expected, 'The expected JSONLD output was found.'); + $expected = $serializer->serialize($entities, 'hal_json'); + $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/hal+json')); + $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.'); } /** diff --git a/core/modules/rest/rest.info.yml b/core/modules/rest/rest.info.yml index e67ea21..1ac5015 100644 --- a/core/modules/rest/rest.info.yml +++ b/core/modules/rest/rest.info.yml @@ -4,7 +4,5 @@ package: Core version: VERSION core: 8.x dependencies: - # @todo Remove this dependency once hard coding to JSON-LD is gone. - - jsonld - serialization configure: admin/config/services/rest diff --git a/core/modules/rest/rest.module b/core/modules/rest/rest.module index 233db32..43b32f4 100644 --- a/core/modules/rest/rest.module +++ b/core/modules/rest/rest.module @@ -69,7 +69,7 @@ function rest_help($path, $arg) { $output .= '
curl -H "Accept: application/vnd.drupal.ld+json" --include --request GET --cookie ' . session_name() . '=' . session_id() . ' ' . url('entity/node/5', array('absolute' => TRUE)) . '
curl -H "Accept: application/hal+json" --include --request GET --cookie ' . session_name() . '=' . session_id() . ' ' . url('entity/node/5', array('absolute' => TRUE)) . '
curl --include --request DELETE --cookie ' . session_name() . '=' . session_id() . ' ' . url('entity/node/5', array('absolute' => TRUE)) . '