Commit bf7d441e authored by Dries's avatar Dries

Issue #1852812 by linclark: Use cache to get entity type/bundle metadata from JSON-LD @type URI.

parent e562b763
......@@ -2,3 +2,4 @@ name = JSON-LD
description = Serializes entities using JSON-LD format.
package = Core
core = 8.x
dependencies[] = rdf
......@@ -8,6 +8,7 @@
namespace Drupal\jsonld;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\Serializer\Serializer;
......@@ -38,6 +39,10 @@ public function build(ContainerBuilder $container) {
'entity' => array(
'jsonld' => 'Drupal\jsonld\JsonldEntityNormalizer',
),
// RDF Schema.
'rdf_schema' => array(
'jsonld' => 'Drupal\jsonld\JsonldRdfSchemaNormalizer',
),
);
// Encoders can only specify which format they support in
// Encoder::supportsEncoding().
......@@ -49,6 +54,8 @@ public function build(ContainerBuilder $container) {
foreach ($normalizers as $supported_class => $formats) {
foreach ($formats as $format => $normalizer_class) {
$container->register("serializer.normalizer.{$supported_class}.{$format}", $normalizer_class)
->addArgument(new Reference('rdf.site_schema_manager'))
->addArgument(new Reference('rdf.mapping_manager'))
->addTag('normalizer', array('priority' => $priority));
}
}
......
......@@ -8,6 +8,8 @@
namespace Drupal\jsonld;
use Drupal\jsonld\JsonldNormalizerBase;
use Drupal\rdf\RdfMappingException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
......@@ -26,36 +28,44 @@ class JsonldEntityNormalizer extends JsonldNormalizerBase implements Denormalize
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
*/
public function normalize($entity, $format = NULL) {
$entityWrapper = new JsonldEntityWrapper($entity, $format, $this->serializer);
$entity_wrapper = new JsonldEntityWrapper($entity, $format, $this->serializer, $this->siteSchemaManager);
$attributes = $entityWrapper->getProperties();
$attributes = $entity_wrapper->getProperties();
$attributes = array(
'@id' => $entityWrapper->getId(),
'@type' => $entityWrapper->getTypeUri(),
'@id' => $entity_wrapper->getId(),
'@type' => $entity_wrapper->getTypeUri(),
) + $attributes;
return $attributes;
}
/**
* Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
*
* @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
*/
public function denormalize($data, $class, $format = null) {
if (!isset($data['@type'])) {
// @todo Throw an exception?
throw new UnexpectedValueException('JSON-LD @type parameter must be included.');
}
// Every bundle has a type, identified by URI. A schema which provides more
// information about the type can be requested from that URI. From that
// schema, the entity type and bundle name are extracted.
$typeUri = is_array($data['@type']) ? $data['@type'][0] : $data['@type'];
// @todo Instead of manually parsing the URI use an approach as discussed in
// http://drupal.org/node/1852812
$parts = explode('/', $typeUri);
$bundle = array_pop($parts);
$entity_type = array_pop($parts);
// Every bundle has a type, identified by URI. The incoming data should
// either include a type URI from this site's schema, or one of the type
// URIs in the incoming data must map to a site schema URI when passed
// through the RDF mapping manager.
$type_uris = is_array($data['@type']) ? $data['@type'] : array($data['@type']);
// If the RDF mapping manager can find a match to a site schema URI, it
// will return the corresponding Typed Data ids. Otherwise, throw an
// exception.
// @todo The @types might be CURIEs or aliases. Expand before trying to map.
try {
$typed_data_ids = $this->rdfMappingManager->getTypedDataIdsFromTypeUris($type_uris);
}
catch (RdfMappingException $e) {
throw new UnexpectedValueException($e->getMessage(), 0, $e);
}
$values = array(
'type' => $bundle,
'type' => $typed_data_ids['bundle'],
);
// If the data specifies a default language, use it to create the entity.
if (isset($data['langcode'])) {
......@@ -68,7 +78,7 @@ public function denormalize($data, $class, $format = null) {
else if ($this->containsTranslation($data)) {
$values['langcode'] = language(LANGUAGE_TYPE_CONTENT)->langcode;
}
$entity = entity_create($entity_type, $values);
$entity = entity_create($typed_data_ids['entity_type'], $values);
// For each attribute in the JSON-LD, add the values as fields to the newly
// created entity. It is assumed that the JSON attribute names are the same
......
......@@ -7,8 +7,10 @@
namespace Drupal\jsonld;
use Drupal\Core\Cache\DatabaseBackend;
use Drupal\Core\Entity\EntityNG;
use Drupal\jsonld\JsonldNormalizerBase;
use Drupal\rdf\SiteSchema\SiteSchemaManager;
use ReflectionClass;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
......@@ -34,9 +36,9 @@ public function normalize($object, $format = NULL) {
// of creating the array of properties, we could simply call normalize and
// pass in the referenced entity with a flag that ensures it is rendered as
// a node reference and not a node definition.
$entityWrapper = new JsonldEntityWrapper($object->entity, $format, $this->serializer);
$entity_wrapper = new JsonldEntityWrapper($object->entity, $format, $this->serializer, $this->siteSchemaManager);
return array(
'@id' => $entityWrapper->getId(),
'@id' => $entity_wrapper->getId(),
);
}
......
......@@ -8,6 +8,9 @@
namespace Drupal\jsonld;
use Drupal\Core\Entity\Entity;
use Drupal\rdf\SiteSchema\SiteSchema;
use Drupal\rdf\SiteSchema\SiteSchemaManager;
use Symfony\Component\Serializer\Serializer;
/**
* Provide an interface for JsonldNormalizer to get required properties.
......@@ -24,7 +27,7 @@ class JsonldEntityWrapper {
/**
* The entity that this object wraps.
*
* @var Drupal\Core\Entity\EntityNG
* @var \Drupal\Core\Entity\EntityNG
*/
protected $entity;
......@@ -42,20 +45,30 @@ class JsonldEntityWrapper {
*/
protected $serializer;
/**
* The site schema manager.
*
* @var \Drupal\rdf\SiteSchema\SiteSchemaManager
*/
protected $siteSchemaManager;
/**
* Constructor.
*
* @param string $entity
* @param \Drupal\Core\Entity\EntityNG $entity
* The Entity API entity
* @param string $format.
* The format.
* @param \Symfony\Component\Serializer\Serializer $serializer
* The serializer, provided by the SerializerAwareNormaizer.
* @param \Drupal\rdf\SiteSchema\SiteSchemaManager $site_schema_manager
* The site schema manager.
*/
public function __construct(Entity $entity, $format, $serializer) {
public function __construct(Entity $entity, $format, Serializer $serializer, SiteSchemaManager $site_schema_manager) {
$this->entity = $entity;
$this->format = $format;
$this->serializer = $serializer;
$this->siteSchemaManager = $site_schema_manager;
}
/**
......@@ -72,13 +85,21 @@ public function getId() {
/**
* Get the type URI.
*
* @todo update or remove this method once the schema dependency to RDF module
* is sorted out.
* @todo Once RdfMappingManager has a mapOutputTypes event, use that instead
* of simply returning the site schema URI.
*/
public function getTypeUri() {
$entity_type = $this->entity->entityType();
$bundle = $this->entity->bundle();
return url('site-schema/content-staging/' . $entity_type . '/' . $bundle, array('absolute' => TRUE));
switch ($this->format) {
case 'drupal_jsonld':
$schema_path = SiteSchema::CONTENT_DEPLOYMENT;
break;
case 'jsonld':
$schema_path = SiteSchema::SYNDICATION;
}
$schema = $this->siteSchemaManager->getSchema($schema_path);
return $schema->bundle($entity_type, $bundle)->getUri();
}
/**
......
......@@ -8,6 +8,8 @@
namespace Drupal\jsonld;
use ReflectionClass;
use Drupal\rdf\RdfMappingManager;
use Drupal\rdf\SiteSchema\SiteSchemaManager;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer;
......@@ -30,6 +32,33 @@ abstract class JsonldNormalizerBase extends SerializerAwareNormalizer implements
*/
static protected $format = array('jsonld', 'drupal_jsonld');
/**
* The site schema manager.
*
* @var \Drupal\rdf\SiteSchema\SiteSchemaManager
*/
protected $siteSchemaManager;
/**
* The RDF mapping manager.
*
* @var \Drupal\rdf\RdfMappingManager
*/
protected $rdfMappingManager;
/**
* Constructor.
*
* @param \Drupal\rdf\SiteSchema\SiteSchemaManager $site_schema_manager
* The site schema manager.
* @param \Drupal\rdf\RdfMappingManager $rdf_mapping_manager
* The RDF mapping manager.
*/
public function __construct(SiteSchemaManager $site_schema_manager, RdfMappingManager $rdf_mapping_manager) {
$this->siteSchemaManager = $site_schema_manager;
$this->rdfMappingManager = $rdf_mapping_manager;
}
/**
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
*/
......
<?php
/**
* @file
* Contains JsonldRdfSchemaNormalizer.
*/
namespace Drupal\jsonld;
use Drupal\jsonld\JsonldNormalizerBase;
use Drupal\rdf\RdfConstants;
/**
* Converts the Drupal entity object structure to JSONLD array structure.
*/
class JsonldRdfSchemaNormalizer extends JsonldNormalizerBase {
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected static $supportedInterfaceOrClass = 'Drupal\rdf\SiteSchema\SchemaTermBase';
/**
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
*/
public function normalize($data, $format = NULL) {
$normalized = array();
$graph = $data->getGraph();
foreach ($graph as $term_uri => $properties) {
// JSON-LD uses the @type keyword as a stand-in for rdf:type. Replace any
// use of rdf:type and move the type to the front of the property array.
if (isset($properties[RdfConstants::RDF_TYPE])) {
$properties = array(
'@type' => $properties[RdfConstants::RDF_TYPE],
) + $properties;
}
unset($properties[RdfConstants::RDF_TYPE]);
// Add the @id keyword to the front of the array.
$normalized[] = array(
'@id' => $term_uri,
) + $properties;
}
return $normalized;
}
}
<?php
/**
* @file
* Contains JsonldTestSetupHelper.
*/
namespace Drupal\jsonld\Tests;
use Drupal\Core\Cache\DatabaseBackend;
use Drupal\jsonld\JsonldEncoder;
use Drupal\jsonld\JsonldEntityNormalizer;
use Drupal\jsonld\JsonldEntityReferenceNormalizer;
use Drupal\jsonld\JsonldFieldItemNormalizer;
use Drupal\rdf\RdfMappingManager;
use Drupal\rdf\EventSubscriber\MappingSubscriber;
use Drupal\rdf\SiteSchema\SiteSchemaManager;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Serializer\Serializer;
/**
* Constructs services for JSON-LD tests.
*/
class JsonldTestSetupHelper {
/**
* The site schema manager.
*
* @var \Drupal\rdf\SiteSchema\SiteSchemaManager
*/
protected $siteSchemaManager;
/**
* The RDF mapping manager.
*
* @var \Drupal\rdf\RdfMappingManager
*/
protected $rdfMappingManager;
/**
* The Normalizer array.
*
* @var array
*/
protected $normalizers;
/**
* Constructor.
*/
public function __construct() {
// Construct site schema manager.
$this->siteSchemaManager = new SiteSchemaManager(new DatabaseBackend('cache'));
// Construct RDF mapping manager.
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new MappingSubscriber());
$this->rdfMappingManager = new RdfMappingManager($dispatcher, $this->siteSchemaManager);
// Construct normalizers.
$this->normalizers = array(
'entityreference' => new JsonldEntityReferenceNormalizer($this->siteSchemaManager, $this->rdfMappingManager),
'field_item' => new JsonldFieldItemNormalizer($this->siteSchemaManager, $this->rdfMappingManager),
'entity' => new JsonldEntityNormalizer($this->siteSchemaManager, $this->rdfMappingManager),
);
$serializer = new Serializer($this->normalizers, array(new JsonldEncoder()));
$this->normalizers['entity']->setSerializer($serializer);
}
/**
* Get Normalizers.
*
* @return array
* An array of normalizers, keyed by supported class or interface.
*/
public function getNormalizers() {
return $this->normalizers;
}
/**
* Get the SiteSchemaManager object.
*
* @return \Drupal\rdf\SiteSchema\SiteSchemaManager
* The SiteSchemaManager, which is also injected into the Normalizers.
*/
public function getSiteSchemaManager() {
return $this->siteSchemaManager;
}
/**
* Get the RdfMappingManager object.
*
* @return \Drupal\rdf\RdfMappingManager
* The RdfMappingManager, which is also injected into the Normalizers.
*/
public function getRdfMappingManager() {
return $this->rdfMappingManager;
}
}
......@@ -8,15 +8,15 @@
namespace Drupal\jsonld\Tests;
use Drupal\Core\Language\Language;
use Drupal\jsonld\JsonldEncoder;
use Drupal\jsonld\JsonldEntityNormalizer;
use Drupal\jsonld\JsonldEntityReferenceNormalizer;
use Drupal\jsonld\JsonldFieldItemNormalizer;
use Drupal\rdf\SiteSchema\SiteSchema;
use Drupal\simpletest\WebTestBase;
use Symfony\Component\Serializer\Serializer;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Test the vendor specific JSON-LD normalizer.
*
* This is implemented as a WebTest because it requires use of the Entity API.
*/
class NormalizeDenormalizeTest extends WebTestBase {
......@@ -25,7 +25,7 @@ class NormalizeDenormalizeTest extends WebTestBase {
*
* @var array
*/
public static $modules = array('language', 'entity_test');
public static $modules = array('entity_test', 'jsonld', 'language', 'rdf');
/**
* The format being tested.
......@@ -51,13 +51,8 @@ public static function getInfo() {
function setUp() {
parent::setUp();
$this->normalizers = array(
'entityreference' => new JsonldEntityReferenceNormalizer(),
'field_item' => new JsonldFieldItemNormalizer(),
'entity' => new JsonldEntityNormalizer(),
);
$serializer = new Serializer($this->normalizers, array(new JsonldEncoder()));
$this->normalizers['entity']->setSerializer($serializer);
$setup_helper = new JsonldTestSetupHelper();
$this->normalizers = $setup_helper->getNormalizers();
// Add German as a language.
$language = new Language(array(
......@@ -146,8 +141,10 @@ public function testNormalize() {
}
function testDenormalize() {
$incomingData = array(
'@type' => url('jsonld-test/content-staging/entity_test/entity_test', array('absolute' => TRUE)),
$schema = new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT);
$bundle_uri = $schema->bundle('entity_test', 'entity_test')->getUri();
$incoming_data = array(
'@type' => $bundle_uri,
'name' => array(
'en' => array(
array(
......@@ -170,17 +167,38 @@ function testDenormalize() {
),
);
$entity = $this->normalizers['entity']->denormalize($incomingData, 'Drupal\Core\Entity\EntityNG', static::$format);
// Test valid request.
$entity = $this->normalizers['entity']->denormalize($incoming_data, 'Drupal\Core\Entity\EntityNG', static::$format);
$this->assertEqual('entity_test', $entity->bundle(), "Denormalize creates entity with correct bundle.");
$this->assertEqual($incomingData['name']['en'], $entity->get('name')->getValue(), "Translatable field denormalized correctly in default language.");
$this->assertEqual($incomingData['name']['de'], $entity->getTranslation('de')->get('name')->getValue(), "Translatable field denormalized correctly in translation language.");
$this->assertEqual($incomingData['field_test_text']['und'], $entity->get('field_test_text')->getValue(), "Untranslatable field denormalized correctly.");
$this->assertEqual($incoming_data['name']['en'], $entity->get('name')->getValue(), "Translatable field denormalized correctly in default language.");
$this->assertEqual($incoming_data['name']['de'], $entity->getTranslation('de')->get('name')->getValue(), "Translatable field denormalized correctly in translation language.");
$this->assertEqual($incoming_data['field_test_text']['und'], $entity->get('field_test_text')->getValue(), "Untranslatable field denormalized correctly.");
// Test request without @type.
unset($incoming_data['@type']);
try {
$this->normalizers['entity']->denormalize($incoming_data, 'Drupal\Core\Entity\EntityNG', static::$format);
$this->fail('Trying to denormalize entity data without @type results in exception.');
}
catch (UnexpectedValueException $e) {
$this->pass('Trying to denormalize entity data without @type results in exception.');
}
// Test request with @type that has no valid mapping.
$incoming_data['@type'] = 'http://failing-uri.com/type';
try {
$this->normalizers['entity']->denormalize($incoming_data, 'Drupal\Core\Entity\EntityNG', static::$format);
$this->fail('Trying to denormalize entity data with unrecognized @type results in exception.');
}
catch (UnexpectedValueException $e) {
$this->pass('Trying to denormalize entity data with unrecognized @type results in exception.');
}
}
/**
* Get the Entity ID.
*
* @param Drupal\Core\Entity\EntityNG $entity
* @param \Drupal\Core\Entity\EntityNG $entity
* Entity to get URI for.
*
* @return string
......
<?php
/**
* @file
* Contains RdfSchemaSerializationTest.
*/
namespace Drupal\jsonld\Tests;
use Drupal\jsonld\JsonldRdfSchemaNormalizer;
use Drupal\jsonld\JsonldEncoder;
use Drupal\rdf\SiteSchema\BundleSchema;
use Drupal\rdf\SiteSchema\SiteSchema;
use Drupal\simpletest\DrupalUnitTestBase;
use Symfony\Component\Serializer\Serializer;
class RdfSchemaSerializationTest extends DrupalUnitTestBase {
public static function getInfo() {
return array(
'name' => 'Site schema JSON-LD serialization',
'description' => 'Tests the JSON-LD serialization of the RDF site schema.',
'group' => 'JSON-LD',
);
}
/**
* Tests the serialization of site schemas.
*/
function testSchemaSerialization() {
// In order to use url() the url_alias table must be installed, so system
// is enabled.
$this->enableModules(array('system'));
$entity_type = $bundle = 'entity_test';
// Set up the bundle schema for the entity_test bundle.
$schema = new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT);
$bundle_schema = $schema->bundle($entity_type, $bundle);
// Set up the serializer.
$setup_helper = new JsonldTestSetupHelper();
$normalizer = new JsonldRdfSchemaNormalizer($setup_helper->getSiteSchemaManager(), $setup_helper->getRdfMappingManager());
$serializer = new Serializer(array($normalizer), array(new JsonldEncoder()));
$serialized = $serializer->serialize($bundle_schema, 'jsonld');
$decoded = json_decode($serialized);
$parsed_term = $decoded[0];
$this->assertEqual($parsed_term->{'@id'}, $bundle_schema->getUri(), 'JSON-LD for schema term uses correct @id.');
$this->assertEqual($parsed_term->{'@type'}, 'http://www.w3.org/2000/01/rdf-schema#class', 'JSON-LD for schema term uses correct @type.');
// The @id and @type should be placed in the beginning of the array.
$array_keys = array_keys((array) $parsed_term);
$this->assertEqual(array('@id', '@type'), array_slice($array_keys, 0, 2), 'JSON-LD keywords are placed before other properties.');
$this->assertTrue(isset($parsed_term->{'http://www.w3.org/2000/01/rdf-schema#isDefinedBy'}), 'Other properties of the term are included.');
}
}
......@@ -8,11 +8,7 @@
namespace Drupal\jsonld\Tests;
use Drupal\config\Tests\ConfigEntityTest;
use Drupal\jsonld\JsonldEntityNormalizer;
use Drupal\jsonld\JsonldEntityReferenceNormalizer;
use Drupal\jsonld\JsonldFieldItemNormalizer;
use Drupal\simpletest\WebTestBase;
use Symfony\Component\Serializer\Serializer;
/**
* Test the vendor specific JSON-LD normalizer.
......@@ -24,7 +20,7 @@ class SupportsSerializationTest extends WebTestBase {
*
* @var array
*/
public static $modules = array('entity_test');
public static $modules = array('entity_test', 'jsonld');
/**
* The format being tested.
......@@ -50,13 +46,8 @@ public static function getInfo() {
function setUp() {
parent::setUp();
$this->normalizers = array(
'entityreference' => new JsonldEntityReferenceNormalizer(),
'field_item' => new JsonldFieldItemNormalizer(),
'entity' => new JsonldEntityNormalizer(),
);
$serializer = new Serializer($this->normalizers);
$this->normalizers['entity']->setSerializer($serializer);
$setup_helper = new JsonldTestSetupHelper();
$this->normalizers = $setup_helper->getNormalizers();
}
/**
......
<?php
/**
* @file
* Contains MappingSubscriber.
*/
namespace Drupal\rdf\EventSubscriber;
use Drupal\rdf\RdfMappingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Default RDF mapping handling.
*/
class MappingSubscriber implements EventSubscriberInterface {
/**
* Stops event if incoming URI is a site schema URI.
*
* If the incoming URI is one of the site's own registered types, then
* mapping is unnecessary. Mapping is only necessary if the incoming type URI
* is from an external vocabulary.
*
* @param \Drupal\rdf\MapTypesFromInputEvent $event
* The mapping event.
*/
public function mapTypesFromInput(\Drupal\rdf\MapTypesFromInputEvent $event) {
$input_uris = $event->getInputUris();
$site_schema_types = $event->getSiteSchemaTypes();
foreach ($input_uris as $input_uri) {
if (isset($site_schema_types[$input_uri])) {
$event->setSiteSchemaUri($input_uri);
$event->stopPropagation();
}
}
}
/**
* Implements EventSubscriberInterface::getSubscribedEvents().
*/
static function getSubscribedEvents() {
$events[RdfMappingEvents::MAP_TYPES_FROM_INPUT] = 'mapTypesFromInput';
return $events;
}
}
<?php
/**
* @file
* Contains RouteSubscriber.
*/
namespace Drupal\rdf\EventSubscriber;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\rdf\SiteSchema\SiteSchema;
use Drupal\rdf\SiteSchema\SiteSchemaManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Route;
/**
* Subscriber for site-generated schema routes.
*/
class RouteSubscriber implements EventSubscriberInterface {
/**
* The site schema manager.
*
* @var \Drupal\rdf\SiteSchema\SiteSchemaManager
*/
protected $siteSchemaManager;
/**
* Constructor.
*
* @param \Drupal\rdf\SiteSchema\SiteSchemaManager $site_schema_manager
* The injected site schema manager.
*/
public function __construct(SiteSchemaManager $site_schema_manager) {
$this->siteSchemaManager = $site_schema_manager;
}
/**
* Adds routes for term types in the site-generated schemas.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The route building event.
*/
public function routes(RouteBuildEvent $event) {
$collection = $event->getRouteCollection();
// Add the routes for all of the terms in both schemas.