diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php index 4d086d1..6ea8ca2 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldBundle.php @@ -38,6 +38,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(). diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php index cfffa8c..8acb212 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php @@ -78,7 +78,7 @@ public function getId() { public function getTypeUri() { $entity_type = $this->entity->entityType(); $bundle = $this->entity->bundle(); - return url('site-schema/content-staging/' . $entity_type . '/' . $bundle, array('absolute' => TRUE)); + return url('site-schema/content-deployment/' . $entity_type . '/' . $bundle, array('absolute' => TRUE)); } /** 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..ab2ed9a --- /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/NormalizeDenormalizeTest.php b/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php index 9ecfda5..e4b1636 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/NormalizeDenormalizeTest.php @@ -147,7 +147,7 @@ public function testNormalize() { function testDenormalize() { $incomingData = array( - '@type' => url('jsonld-test/content-staging/entity_test/entity_test', array('absolute' => TRUE)), + '@type' => url('jsonld-test/content-deployment/entity_test/entity_test', array('absolute' => TRUE)), 'name' => array( 'en' => array( array( 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..474dd94 --- /dev/null +++ b/core/modules/jsonld/lib/Drupal/jsonld/Tests/RdfSchemaSerializationTest.php @@ -0,0 +1,51 @@ + '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 BundleSchema(new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT), $entityType, $bundle); + // Set up the serializer. + $serializer = new Serializer(array(new JsonldRdfSchemaNormalizer()), array(new JsonldEncoder())); + + $serialized = $serializer->serialize($schema, 'jsonld'); + $decoded = json_decode($serialized); + $parsedTerm = $decoded[0]; + + $this->assertEqual($parsedTerm->{'@id'}, $schema->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/rdf/lib/Drupal/rdf/EventSubscriber/MappingSubscriber.php b/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/MappingSubscriber.php new file mode 100644 index 0000000..f08a89f --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/MappingSubscriber.php @@ -0,0 +1,34 @@ +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_INPUT_TYPES] = 'mapInputTypes'; + 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..ea045ca --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/EventSubscriber/RouteSubscriber.php @@ -0,0 +1,65 @@ +getRouteCollection(); + + // The paths of the site-generated schemas. + $schemaPaths = array( + SiteSchema::CONTENT_DEPLOYMENT, + SiteSchema::SYNDICATION, + ); + + // Add the routes for all of the terms in both schemas. + foreach ($schemaPaths as $schemaPath) { + $schema = new SiteSchema($schemaPath); + $routes = $schema->getRoutes(); + foreach ($routes as $controller => $pattern) { + $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/MapInputTypesEvent.php b/core/modules/rdf/lib/Drupal/rdf/MapInputTypesEvent.php new file mode 100644 index 0000000..20be166 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/MapInputTypesEvent.php @@ -0,0 +1,90 @@ +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..d08ff88 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/RdfBundle.php @@ -0,0 +1,34 @@ +register('rdf.mapping', 'Drupal\rdf\EventSubscriber\MappingSubscriber') + ->addTag('event_subscriber'); + // Route subscriber. + $container->register('rdf.route_subscriber', 'Drupal\rdf\EventSubscriber\RouteSubscriber') + ->addTag('event_subscriber'); + + // Mapping manager service. + $container->register('rdf.site_schema.mapping_manager', 'Drupal\rdf\RdfMappingManager') + ->addArgument(new Reference('dispatcher')); + } +} 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..f970e44 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/RdfConstants.php @@ -0,0 +1,26 @@ +dispatcher = $dispatcher; + $this->siteSchemas = array( + new SiteSchema(SiteSchema::CONTENT_DEPLOYMENT), + new SiteSchema(SiteSchema::SYNDICATION), + ); + } + + /** + * 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. + */ + public function getTypedDataIds($inputRdfTypes) { + // Get the cache of site schema types. + $siteSchemaTypes = $this->getCacheData('rdf:site_schema:types'); + // Map the RDF type from the incoming data to an RDF type defined in the + // internal site schema. + $typeUri = $this->mapInputTypes($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) { + // @todo Throw exception. + } + // 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]; + } + + /** + * Get the cached array of RDF terms and corresponding TypedData Ids. + * + * @param string $bin + * The cache to get (e.g. rdf:site_schema:types). + * + * @return array + * The cached data. + */ + protected function getCacheData($bin) { + $cache = cache()->get($bin); + // If the cache is empty, build it. + if ($cache == FALSE) { + switch ($bin) { + case 'rdf:site_schema:types': + $data = $this->buildTypeCache(); + break; + } + cache()->set($bin, $data, CacheBackendInterface::CACHE_PERMANENT, 'entity_info'); + return $data; + } + return $cache->data; + } + + /** + * Build the site schema type cache. + * + * @return array + * An array of TypedData API IDs, keyed by corresponding site schema URI. + */ + protected function buildTypeCache() { + $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\Entity\ContentEntityInterface')) { + 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) { + $bundleSchema = new BundleSchema($schema, $entityType, $bundle); + $bundleUri = $bundleSchema->getUri(); + $data[$bundleUri] = array( + 'entity_type' => $entityType, + 'bundle' => $bundle, + ); + } + } + } + return $data; + } + + /** + * 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 mapInputTypes($inputRdfTypes) { + // Create the event using the array of incoming RDF type URIs and the cache + // of internal site schema URIs. + $siteSchemaTypes = $this->getCacheData('rdf:site_schema:types'); + $typeMap = new MapInputTypesEvent($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_INPUT_TYPES, $typeMap); + + return $typeMap->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..4a04222 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php @@ -0,0 +1,59 @@ +bundle = $bundle; + } + + /** + * Implements \Drupal\rdf\SiteSchema\SchemaBase::getUri(). + */ + public function getUri() { + $path = str_replace(array('{entity_type}', '{bundle}'), array($this->entityType, $this->bundle), self::URI_PATTERN); + return $this->siteSchema->getUri() . $path; + } + + /** + * Overrides \Drupal\rdf\SiteSchema\SchemaBase::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..6266173 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php @@ -0,0 +1,68 @@ +entityType = $entity_type; + } + + /** + * Implements \Drupal\rdf\SiteSchema\SchemaBase::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\SchemaBase::getUri(). + */ + public function getUri() { + $path = str_replace('{entity_type}', $this->entityType , self::URI_PATTERN); + return $this->siteSchema->getUri() . $path; + } + + /** + * Overrides \Drupal\rdf\SiteSchema\SchemaBase::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/SchemaBase.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaBase.php new file mode 100644 index 0000000..11ac3db --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaBase.php @@ -0,0 +1,69 @@ +siteSchema = $siteSchema; + } + + /** + * Get the full graph of terms and properties to display. + * + * When an RDF term URI is dereferenced, it usually contains a description of + * the term in RDF. To make it easier to use this description, include + * information about all related terms. For example, when viewing the RDF + * description for the RDF class which corresponds to a Drupal bundle, data + * about its fields would also be included. + */ + abstract public function getGraph(); + + /** + * Get the term properties. + * + * @return array + * An array of properties for this term, keyed by URI. + */ + public function getProperties() { + return array( + RdfConstants::RDFS_IS_DEFINED_BY => $this->siteSchema->getUri(), + ); + } + + /** + * Get the URI of the term. + * + * Implementations of this method will use the URI patterns defined in + * URI_PATTERN constants and replace placeholders with actual values. + * + * @return string + * The URI of the term. + */ + abstract public function getUri(); + +} 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..9a60bc6 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SchemaController.php @@ -0,0 +1,49 @@ + $entity_type))); + } + if (!array_key_exists($bundle, $entity_info['bundles'])) { + throw new NotFoundHttpException(t('Bundle @bundle not found', array('@bundle' => $bundle))); + } + + $serializer = drupal_container()->get('serializer'); + $siteSchema = new SiteSchema($schema_path); + // @todo Remove hard-coded mimetype once we have proper conneg. + $content = $serializer->serialize($siteSchema->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/SiteSchema.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SiteSchema.php new file mode 100644 index 0000000..ec15225 --- /dev/null +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/SiteSchema.php @@ -0,0 +1,73 @@ +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 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::URI_PATTERN, + ); + } +} 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..f0e55f0 --- /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.'); + } + +}