diff --git a/core/modules/jsonld/jsonld.info b/core/modules/jsonld/jsonld.info index 70d5d40..110b881 100644 --- a/core/modules/jsonld/jsonld.info +++ b/core/modules/jsonld/jsonld.info @@ -2,3 +2,4 @@ name = JSON-LD description = Serializes entities using JSON-LD format. package = Core core = 8.x +dependencies[] = rdf diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php index 538f4d0..00f1433 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php @@ -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)); } } diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php index 2ab909e..c0d06aa 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityNormalizer.php @@ -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,7 +28,7 @@ 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); + $entityWrapper = new JsonldEntityWrapper($entity, $format, $this->serializer, $this->siteSchemaManager); $attributes = $entityWrapper->getProperties(); $attributes = array( @@ -38,24 +40,32 @@ public function normalize($entity, $format = NULL) { /** * 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. + $typeUris = 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 { + $typedDataIds = $this->rdfMappingManager->getTypedDataIdsFromTypeUris($typeUris); + } + catch (RdfMappingException $e) { + throw new UnexpectedValueException($e->getMessage(), 0, $e); + } $values = array( - 'type' => $bundle, + 'type' => $typedDataIds['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($typedDataIds['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 diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php index e28ce95..723b90c 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityReferenceNormalizer.php @@ -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,7 +36,7 @@ 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); + $entityWrapper = new JsonldEntityWrapper($object->entity, $format, $this->serializer, $this->siteSchemaManager); return array( '@id' => $entityWrapper->getId(), ); diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php index cfffa8c..19c7514 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php @@ -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; @@ -43,19 +46,29 @@ 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 $siteSchemaManager + * The site schema manager. */ - public function __construct(Entity $entity, $format, $serializer) { + public function __construct(Entity $entity, $format, Serializer $serializer, SiteSchemaManager $siteSchemaManager) { $this->entity = $entity; $this->format = $format; $this->serializer = $serializer; + $this->siteSchemaManager = $siteSchemaManager; } /** @@ -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(); } /** diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php index bb9ae5a..b3774e0 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldNormalizerBase.php @@ -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; @@ -31,6 +33,33 @@ 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 $siteSchemaManager + * The site schema manager. + * @param \Drupal\rdf\RdfMappingManager $rdfMappingManager + * The RDF mapping manager. + */ + public function __construct(SiteSchemaManager $siteSchemaManager, RdfMappingManager $rdfMappingManager) { + $this->siteSchemaManager = $siteSchemaManager; + $this->rdfMappingManager = $rdfMappingManager; + } + + /** * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() */ public function supportsNormalization($data, $format = NULL) { diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldRdfSchemaNormalizer.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldRdfSchemaNormalizer.php new file mode 100644 index 0000000..0196dcf --- /dev/null +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldRdfSchemaNormalizer.php @@ -0,0 +1,51 @@ +getGraph(); + + foreach ($graph as $termUri => $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' => $termUri, + ) + $properties; + } + + return $normalized; + } + +} diff --git a/core/modules/jsonld/lib/Drupal/jsonld/Tests/JsonldTestSetupHelper.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/JsonldTestSetupHelper.php new file mode 100644 index 0000000..d0eb0c3 --- /dev/null +++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/JsonldTestSetupHelper.php @@ -0,0 +1,96 @@ +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; + } +} diff --git a/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php index 9ecfda5..44bd3173 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php @@ -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); + $setupHelper = new JsonldTestSetupHelper(); + $this->normalizers = $setupHelper->getNormalizers(); // Add German as a language. $language = new Language(array( @@ -146,8 +141,10 @@ public function testNormalize() { } function testDenormalize() { + $schema = new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT); + $bundleUri = $schema->bundle('entity_test', 'entity_test')->getUri(); $incomingData = array( - '@type' => url('jsonld-test/content-staging/entity_test/entity_test', array('absolute' => TRUE)), + '@type' => $bundleUri, 'name' => array( 'en' => array( array( @@ -170,17 +167,38 @@ function testDenormalize() { ), ); + // Test valid request. $entity = $this->normalizers['entity']->denormalize($incomingData, '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."); + + // Test request without @type. + unset($incomingData['@type']); + try { + $this->normalizers['entity']->denormalize($incomingData, '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. + $incomingData['@type'] = 'http://failing-uri.com/type'; + try { + $this->normalizers['entity']->denormalize($incomingData, '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 diff --git a/core/modules/jsonld/lib/Drupal/jsonld/Tests/RdfSchemaSerializationTest.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/RdfSchemaSerializationTest.php new file mode 100644 index 0000000..27a8d79 --- /dev/null +++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/RdfSchemaSerializationTest.php @@ -0,0 +1,55 @@ + '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')); + + $entityType = $bundle = 'entity_test'; + + // Set up the bundle schema for the entity_test bundle. + $schema = new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT); + $bundleSchema = $schema->bundle($entityType, $bundle); + // Set up the serializer. + $setupHelper = new JsonldTestSetupHelper(); + $normalizer = new JsonldRdfSchemaNormalizer($setupHelper->getSiteSchemaManager(), $setupHelper->getRdfMappingManager()); + $serializer = new Serializer(array($normalizer), array(new JsonldEncoder())); + + $serialized = $serializer->serialize($bundleSchema, 'jsonld'); + $decoded = json_decode($serialized); + $parsedTerm = $decoded[0]; + + $this->assertEqual($parsedTerm->{'@id'}, $bundleSchema->getUri(), 'JSON-LD for schema term uses correct @id.'); + $this->assertEqual($parsedTerm->{'@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. + $arrayKeys = array_keys((array) $parsedTerm); + $this->assertEqual(array('@id', '@type'), array_slice($arrayKeys, 0, 2), 'JSON-LD keywords are placed before other properties.'); + $this->assertTrue(isset($parsedTerm->{'http://www.w3.org/2000/01/rdf-schema#isDefinedBy'}), 'Other properties of the term are included.'); + } +} diff --git a/core/modules/jsonld/lib/Drupal/jsonld/Tests/SupportsSerializationTest.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/SupportsSerializationTest.php index b0d4474..f4ac546 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/Tests/SupportsSerializationTest.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/SupportsSerializationTest.php @@ -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); + $setupHelper = new JsonldTestSetupHelper(); + $this->normalizers = $setupHelper->getNormalizers(); } /** diff --git a/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/MappingSubscriber.php b/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/MappingSubscriber.php new file mode 100644 index 0000000..de1d7e9 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/MappingSubscriber.php @@ -0,0 +1,45 @@ +getInputUris(); + $siteSchemaTypes = $event->getSiteSchemaTypes(); + foreach ($inputUris as $inputUri) { + if (isset($siteSchemaTypes[$inputUri])) { + $event->setSiteSchemaUri($inputUri); + $event->stopPropagation(); + } + } + } + + /** + * Implements EventSubscriberInterface::getSubscribedEvents(). + */ + static function getSubscribedEvents() { + $events[RdfMappingEvents::MAP_TYPES_FROM_INPUT] = 'mapTypesFromInput'; + return $events; + } +} diff --git a/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/RouteSubscriber.php b/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/RouteSubscriber.php new file mode 100644 index 0000000..0c6d60a --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/RouteSubscriber.php @@ -0,0 +1,78 @@ +siteSchemaManager = $siteSchemaManager; + } + + /** + * 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. + foreach ($this->siteSchemaManager->getSchemas() as $schema) { + $routes = $schema->getRoutes(); + foreach ($routes as $controller => $pattern) { + $schemaPath = $schema->getPath(); + $route = new Route($pattern, array( + '_controller' => 'Drupal\rdf\SiteSchema\SchemaController::' . $controller, + 'schema_path' => $schemaPath, + ), array( + '_method' => 'GET', + '_access' => 'TRUE', + )); + // Create the route name to use in the RouteCollection. Remove the + // trailing slash and replace characters, so that a path such as + // site-schema/syndication/ becomes rdf.site_schema.syndication. + $routeName = 'rdf.' . str_replace(array('-','/'), array('_', '.'), substr_replace($schemaPath ,"",-1)); + $collection->add($routeName, $route); + } + } + } + + /** + * Implements EventSubscriberInterface::getSubscribedEvents(). + */ + static function getSubscribedEvents() { + $events[RoutingEvents::DYNAMIC] = 'routes'; + return $events; + } +} + diff --git a/core/modules/rdf/lib/Drupal/rdf/MapTypesFromInputEvent.php b/core/modules/rdf/lib/Drupal/rdf/MapTypesFromInputEvent.php new file mode 100644 index 0000000..6b0e841 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/MapTypesFromInputEvent.php @@ -0,0 +1,91 @@ +inputUris = $inputUris; + $this->siteSchemaTypes = $siteSchemaTypes; + $this->siteSchemaUri = FALSE; + } + + /** + * Gets the input URI. + * + * @return array + * The array of incoming RDF type URIs. + */ + public function getInputUris() { + return $this->inputUris; + } + + /** + * Gets the cache of internal site schema types. + * + * @return array + * The cached site schema type array. + */ + public function getSiteSchemaTypes() { + return $this->siteSchemaTypes; + } + + /** + * Gets the site schema URI. + * + * @return string|bool + * The site schema type URI if set, FALSE if otherwise. + */ + public function getSiteSchemaUri() { + return $this->siteSchemaUri; + } + + /** + * Sets the site schema URI. + * + * @param string $uri + * The site schema type URI. + */ + public function setSiteSchemaUri($uri) { + $this->siteSchemaUri = $uri; + } +} diff --git a/core/modules/rdf/lib/Drupal/rdf/RdfBundle.php b/core/modules/rdf/lib/Drupal/rdf/RdfBundle.php new file mode 100644 index 0000000..258ab6d --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/RdfBundle.php @@ -0,0 +1,45 @@ +register('cache.rdf.site_schema.types', 'Drupal\Core\Cache\CacheBackendInterface') + ->setFactoryClass('Drupal\Core\Cache\CacheFactory') + ->setFactoryMethod('get') + ->addArgument('cache'); + + // Site schema manager service. + $container->register('rdf.site_schema_manager', 'Drupal\rdf\SiteSchema\SiteSchemaManager') + ->addArgument(new Reference('cache.rdf.site_schema.types')); + // Mapping manager service. + $container->register('rdf.mapping_manager', 'Drupal\rdf\RdfMappingManager') + ->addArgument(new Reference('dispatcher')) + ->addArgument(new Reference('rdf.site_schema_manager')); + + // Mapping subscriber. + $container->register('rdf.mapping', 'Drupal\rdf\EventSubscriber\MappingSubscriber') + ->addTag('event_subscriber'); + // Route subscriber. + $container->register('rdf.route_subscriber', 'Drupal\rdf\EventSubscriber\RouteSubscriber') + ->addArgument(new Reference('rdf.site_schema_manager')) + ->addTag('event_subscriber'); + } +} diff --git a/core/modules/rdf/lib/Drupal/rdf/RdfConstants.php b/core/modules/rdf/lib/Drupal/rdf/RdfConstants.php new file mode 100644 index 0000000..b64ae91 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/RdfConstants.php @@ -0,0 +1,26 @@ +dispatcher = $dispatcher; + $this->siteSchemaManager = $siteSchemaManager; + } + + /** + * Convert an array of RDF type URIs to the corresponding TypedData IDs. + * + * @param array $inputRdfTypes + * An array of URIs for the type. + * + * @return array + * An array containing entity_type and bundle. + * + * @throws \Drupal\rdf\RdfMappingException + */ + public function getTypedDataIdsFromTypeUris($inputRdfTypes) { + // Get the cache of site schema types. + $siteSchemaTypes = $this->siteSchemaManager->getTypes(); + // Map the RDF type from the incoming data to an RDF type defined in the + // internal site schema. + $typeUri = $this->mapTypesFromInput($inputRdfTypes); + // If no site schema URI has been determined, then it's impossible to know + // what entity type to create. Throw an exception. + if ($typeUri == FALSE) { + throw new RdfMappingException(sprintf('No mapping to a site schema type URI found for incoming types (%s).', implode(',', $inputRdfTypes))); + } + // Use the mapped RDF type URI to get the TypedData API ids the rest of the + // system uses (entity type and bundle). + return $siteSchemaTypes[$typeUri]; + } + + /** + * Map an array of incoming URIs to an internal site schema URI. + * + * @param array $inputRdfTypes + * An array of RDF type URIs. + * + * @return string + * The corresponding site schema type URI. + */ + protected function mapTypesFromInput($inputRdfTypes) { + // Create the event using the array of incoming RDF type URIs and the cache + // of internal site schema URIs. + $siteSchemaTypes = $this->siteSchemaManager->getTypes(); + $mapping_event = new MapTypesFromInputEvent($inputRdfTypes, $siteSchemaTypes); + + // Allow other modules to map the incoming type URIs to an internal site + // schema type URI. For example, a content deployment module could take + // URIs from the staging site's schema and map them to the corresponding + // URI in the live site's schema. + $this->dispatcher->dispatch(RdfMappingEvents::MAP_TYPES_FROM_INPUT, $mapping_event); + + return $mapping_event->getSiteSchemaUri(); + } +} diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php new file mode 100644 index 0000000..7c9196d --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php @@ -0,0 +1,64 @@ +bundle = $bundle; + } + + /** + * Implements \Drupal\rdf\SiteSchema\SchemaTermInterface::getUri(). + */ + public function getUri() { + $path = str_replace(array('{entity_type}', '{bundle}'), array($this->entityType, $this->bundle), static::$uriPattern); + return $this->siteSchema->getUri() . $path; + } + + /** + * Overrides \Drupal\rdf\SiteSchema\SchemaTermBase::getProperties(). + */ + public function getProperties() { + $properties = parent::getProperties(); + $properties[RdfConstants::RDFS_SUB_CLASS_OF] = $this->siteSchema->entity($this->entityType)->getUri(); + return $properties; + } + +} diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php new file mode 100644 index 0000000..ec72219 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php @@ -0,0 +1,73 @@ +entityType = $entity_type; + } + + /** + * Implements \Drupal\rdf\SiteSchema\SchemaTermInterface::getGraph(). + * + * @todo Loop through all fields and add their RDF descriptions. + */ + public function getGraph() { + $graph = array(); + $graph[$this->getUri()] = $this->getProperties(); + return $graph; + } + + /** + * Implements \Drupal\rdf\SiteSchema\SchemaTermInterface::getUri(). + */ + public function getUri() { + $path = str_replace('{entity_type}', $this->entityType , static::$uriPattern); + return $this->siteSchema->getUri() . $path; + } + + /** + * Overrides \Drupal\rdf\SiteSchema\SchemaTermBase::getProperties(). + */ + public function getProperties() { + $properties = parent::getProperties(); + $properties[RdfConstants::RDF_TYPE] = RdfConstants::RDFS_CLASS; + return $properties; + } + +} diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaController.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaController.php new file mode 100644 index 0000000..93abb9d --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaController.php @@ -0,0 +1,69 @@ +container = $container; + } + + /** + * Responds to a schema request for a bundle of a given entity type. + * + * @param string $entity_type + * The entity type. + * @param string $bundle + * The entity bundle. + * @param string $schema_path + * The relative base path for the schema. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response object. + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function bundle($entity_type, $bundle, $schema_path) { + if (!$entity_info = entity_get_info($entity_type)) { + throw new NotFoundHttpException(t('Entity type @entity_type not found', array('@entity_type' => $entity_type))); + } + if (!array_key_exists($bundle, $entity_info['bundles'])) { + throw new NotFoundHttpException(t('Bundle @bundle not found', array('@bundle' => $bundle))); + } + + $serializer = $this->container->get('serializer'); + $siteSchemaManager = $this->container->get('rdf.site_schema_manager'); + $schema = $siteSchemaManager->getSchema($schema_path); + // @todo Remove hard-coded mimetype once we have proper conneg. + $content = $serializer->serialize($schema->bundle($entity_type, $bundle), 'jsonld'); + return new Response($content, 200, array('Content-type' => 'application/json')); + } + +} diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaTermBase.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaTermBase.php new file mode 100644 index 0000000..abff580 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaTermBase.php @@ -0,0 +1,50 @@ +siteSchema = $siteSchema; + } + + /** + * Implements \Drupal\rdf\SiteSchema\SchemaTermInterface::getProperties(). + */ + public function getProperties() { + return array( + RdfConstants::RDFS_IS_DEFINED_BY => $this->siteSchema->getUri(), + ); + } + +} diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaTermInterface.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaTermInterface.php new file mode 100644 index 0000000..342b51e --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaTermInterface.php @@ -0,0 +1,44 @@ +schemaPath = $schemaPath; + } + + /** + * Get an entity's term definition in this vocabulary. + */ + public function entity($entity_type) { + return new EntitySchema($this, $entity_type); + } + + /** + * Get a bundle's term definition in this vocabulary. + */ + public function bundle($entity_type, $bundle) { + return new BundleSchema($this, $entity_type, $bundle); + } + + /** + * Get the URI of the schema. + * + * @return string + * The URI of the schema. + */ + public function getUri() { + return url($this->schemaPath, array('absolute' => TRUE)); + } + + /** + * Get the relative base path of the schema. + */ + public function getPath() { + return $this->schemaPath; + } + + /** + * Get the routes for the types of terms defined in this schema. + * + * @return array + * An array of route patterns, keyed by controller method name. + */ + public function getRoutes() { + return array( + 'bundle' => $this->schemaPath . BundleSchema::$uriPattern, + ); + } +} diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SiteSchemaManager.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SiteSchemaManager.php new file mode 100644 index 0000000..639b221 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SiteSchemaManager.php @@ -0,0 +1,89 @@ +cache = $cache; + $this->siteSchemas = array( + SiteSchema::CONTENT_DEPLOYMENT => new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT), + SiteSchema::SYNDICATION => new SiteSchema(SiteSchema::SYNDICATION), + ); + } + + /** + * Writes the cache of site schema types. + */ + public function writeCache() { + $data = array(); + + // Type URIs correspond to bundles. Iterate through the bundles to get the + // URI and data for them. + foreach (entity_get_info() as $entityType => $entityInfo) { + // Only content entities are supported currently. + // @todo Consider supporting config entities. + $reflection = new ReflectionClass($entityInfo['class']); + if ($reflection->implementsInterface('\Drupal\Core\Config\Entity\ConfigEntityInterface')) { + continue; + } + foreach ($entityInfo['bundles'] as $bundle => $bundleInfo) { + // Get a type URI for the bundle in each of the defined schemas. + foreach ($this->siteSchemas as $schema) { + $bundleUri = $schema->bundle($entityType, $bundle)->getUri(); + $data[$bundleUri] = array( + 'entity_type' => $entityType, + '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('rdf:site_schema:types', $data, CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => TRUE)); + } + + public function getSchemas() { + return $this->siteSchemas; + } + + public function getSchema($schemaPath) { + return $this->siteSchemas[$schemaPath]; + } + + /** + * Get the array of site schema types. + * + * @return array + * An array of typed data ids (entity_type and bundle) keyed by + * corresponding site schema URI. + */ + public function getTypes() { + $cid = 'rdf:site_schema:types'; + $cache = $this->cache->get($cid); + if (!$cache) { + $this->writeCache(); + $cache = $this->cache->get($cid); + } + return $cache->data; + } + +} diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/RdfMappingEventTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/RdfMappingEventTest.php new file mode 100644 index 0000000..d71a652 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/Tests/RdfMappingEventTest.php @@ -0,0 +1,64 @@ + 'RDF mapping tests', + 'description' => 'Tests the event-based RDF mapping system.', + 'group' => 'RDF', + ); + } + + /** + * Test that other modules can define incoming type mappings. + */ + public function testMapInputType() { + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new MappingSubscriber()); + $dispatcher->addSubscriber(new TestMappingSubscriber()); + $siteSchemaManager = new SiteSchemaManager(new DatabaseBackend('cache')); + $mappingManager = new RdfMappingManager($dispatcher, $siteSchemaManager); + + // Test that a site schema URI is mapped to itself. This is the default + // behavior. + $schema = new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT); + $bundleSchema = $schema->bundle('entity_test', 'entity_test'); + $siteSchemaType = $bundleSchema->getUri(); + $typedDataIds = $mappingManager->getTypedDataIdsFromTypeUris(array($siteSchemaType)); + $this->assertTrue($typedDataIds['bundle'] == 'entity_test', 'An internal site schema type URI is properly handled.'); + + // Test that a module can map an external URI to a site schema URI. + $typedDataIds = $mappingManager->getTypedDataIdsFromTypeUris(array(TestMappingSubscriber::STAGING_SITE_TYPE_URI)); + $this->assertTrue($typedDataIds['bundle'] == 'entity_test', 'Modules can map external type URIs to a site schema type.'); + } + +} diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/SiteSchemaTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/SiteSchemaTest.php new file mode 100644 index 0000000..fd1bb19 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/Tests/SiteSchemaTest.php @@ -0,0 +1,55 @@ + 'RDF site schema test', + 'description' => 'Confirm that site-generated schemas are created for entity, bundle, field, and field property.', + 'group' => 'RDF', + ); + } + + /** + * Tests site-generated schema. + */ + function testSiteSchema() { + $entityType = $bundle = 'entity_test'; + $schema = new SiteSchema(SiteSchema::SYNDICATION); + $schemaPath = 'site-schema/syndication/'; + + // Bundle. + $bundleSchema = $schema->bundle($entityType, $bundle); + $bundleUri = url("$schemaPath$entityType/$bundle", array('absolute' => TRUE)); + $bundleProperties = array( + 'http://www.w3.org/2000/01/rdf-schema#isDefinedBy' => url($schemaPath, array('absolute' => TRUE)), + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' => 'http://www.w3.org/2000/01/rdf-schema#class', + 'http://www.w3.org/2000/01/rdf-schema#subClassOf' => url("$schemaPath$entityType", array('absolute' => TRUE)), + ); + + $this->assertEqual($bundleSchema->getUri(), $bundleUri, 'Bundle term URI is generated correctly.'); + $this->assertEqual($bundleSchema->getProperties(), $bundleProperties, 'Bundle term properties are generated correctly.'); + } + +} diff --git a/core/modules/rdf/tests/rdf_test_mapping/lib/Drupal/rdf_test_mapping/EventSubscriber/TestMappingSubscriber.php b/core/modules/rdf/tests/rdf_test_mapping/lib/Drupal/rdf_test_mapping/EventSubscriber/TestMappingSubscriber.php new file mode 100644 index 0000000..1141615 --- /dev/null +++ b/core/modules/rdf/tests/rdf_test_mapping/lib/Drupal/rdf_test_mapping/EventSubscriber/TestMappingSubscriber.php @@ -0,0 +1,58 @@ +getInputUris(); + $siteSchemaTypes = $event->getSiteSchemaTypes(); + + // This mapping between an external type and a site schema type would be + // managed by something in the implementing module, such as a database + // table. For the test, manually map a fake external URI to the site schema + // URI for the test entity. + $schema = new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT); + $bundleSchema = $schema->bundle('entity_test', 'entity_test'); + $siteSchemaType = $bundleSchema->getUri(); + $mapping = array( + self::STAGING_SITE_TYPE_URI => $siteSchemaType, + ); + + foreach ($inputUris as $inputUri) { + // If the incoming URI is mapped in the mapping array, and the value of + // that mapping is found in the cache of site schema types, then set the + // site schema URI. + if (isset($mapping[$inputUri]) && isset($siteSchemaTypes[$mapping[$inputUri]])) { + $event->setSiteSchemaUri($mapping[$inputUri]); + } + } + } + + /** + * Implements EventSubscriberInterface::getSubscribedEvents(). + */ + static function getSubscribedEvents() { + $events[RdfMappingEvents::MAP_TYPES_FROM_INPUT] = 'mapTypesFromInput'; + return $events; + } + +} diff --git a/core/modules/rdf/tests/rdf_test_mapping/lib/Drupal/rdf_test_mapping/RdfTestMappingBundle.php b/core/modules/rdf/tests/rdf_test_mapping/lib/Drupal/rdf_test_mapping/RdfTestMappingBundle.php new file mode 100644 index 0000000..3de3980 --- /dev/null +++ b/core/modules/rdf/tests/rdf_test_mapping/lib/Drupal/rdf_test_mapping/RdfTestMappingBundle.php @@ -0,0 +1,27 @@ +register('rdf_test_mapping.mapping', 'Drupal\rdf_test_mapping\EventSubscriber\TestMappingSubscriber') + ->addTag('event_subscriber'); + } + +} diff --git a/core/modules/rdf/tests/rdf_test_mapping/rdf_test_mapping.info b/core/modules/rdf/tests/rdf_test_mapping/rdf_test_mapping.info new file mode 100644 index 0000000..d04be41 --- /dev/null +++ b/core/modules/rdf/tests/rdf_test_mapping/rdf_test_mapping.info @@ -0,0 +1,6 @@ +name = "RDF module mapping test" +description = "Test mapping subscriber for RDF mapping tests." +package = Testing +core = 8.x +hidden = TRUE +dependencies[] = rdf diff --git a/core/modules/rdf/tests/rdf_test_mapping/rdf_test_mapping.module b/core/modules/rdf/tests/rdf_test_mapping/rdf_test_mapping.module new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/core/modules/rdf/tests/rdf_test_mapping/rdf_test_mapping.module @@ -0,0 +1 @@ +