diff --git a/islandora.services.yml b/islandora.services.yml index 64e8753d..ccd4dd89 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -24,6 +24,3 @@ services: logger.channel.islandora: parent: logger.channel_base arguments: ['islandora'] - islandora.jsonldcontextgenerator: - class: Drupal\islandora\JsonldContextGenerator\JsonldContextGenerator - arguments: ['@entity_field.manager','@entity_type.bundle.info','@entity_type.manager', '@cache.default', '@logger.channel.islandora'] diff --git a/src/Controller/FedoraResourceJsonLdContextController.php b/src/Controller/FedoraResourceJsonLdContextController.php index 3a4b5474..77c41ac0 100644 --- a/src/Controller/FedoraResourceJsonLdContextController.php +++ b/src/Controller/FedoraResourceJsonLdContextController.php @@ -4,7 +4,7 @@ namespace Drupal\islandora\Controller; use Drupal\Core\Cache\CacheableJsonResponse; use Drupal\Core\Controller\ControllerBase; -use Drupal\islandora\JsonldContextGenerator\JsonldContextGeneratorInterface; +use Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -22,14 +22,14 @@ class FedoraResourceJsonLdContextController extends ControllerBase { /** * Injected JsonldContextGenerator. * - * @var \Drupal\islandora\JsonldContextGenerator\JsonldContextGeneratorInterface + * @var \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface */ private $jsonldContextGenerator; /** * FedoraResourceJsonLdContextController constructor. * - * @param \Drupal\islandora\JsonldContextGenerator\JsonldContextGeneratorInterface $jsonld_context_generator + * @param \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface $jsonld_context_generator * Injected JsonldContextGenerator. */ public function __construct(JsonldContextGeneratorInterface $jsonld_context_generator) { @@ -43,10 +43,10 @@ class FedoraResourceJsonLdContextController extends ControllerBase { * The App Container. * * @return static - * An instance of our islandora.jsonldcontextgenerator service. + * An instance of our jsonld.contextgenerator service. */ public static function create(ContainerInterface $container) { - return new static($container->get('islandora.jsonldcontextgenerator')); + return new static($container->get('jsonld.contextgenerator')); } /** diff --git a/src/JsonldContextGenerator/JsonldContextGenerator.php b/src/JsonldContextGenerator/JsonldContextGenerator.php deleted file mode 100644 index e9161df2..00000000 --- a/src/JsonldContextGenerator/JsonldContextGenerator.php +++ /dev/null @@ -1,424 +0,0 @@ -entityFieldManager = $entity_field_manager; - $this->entityTypeManager = $entity_manager; - $this->bundleInfo = $bundle_info; - $this->cache = $cache_backend; - $this->logger = $logger_channel; - } - - /** - * {@inheritdoc} - */ - public function getContext($ids = 'fedora_resource.rdf_source') { - $cid = JsonldContextGenerator::CACHE_BASE_CID . $ids; - $cache = $this->cache->get($cid); - $data = ''; - if (!$cache) { - $rdfMapping = RdfMapping::load($ids); - // Our whole chain of exceptions will never happen - // because RdfMapping:load returns NULL on non existance - // Which forces me to check for it - // and don't even call writeCache on missing - // Solution, throw also one here. - if ($rdfMapping) { - $data = $this->writeCache($rdfMapping, $cid); - } - else { - $msg = t("Can't generate JSON-LD Context for @ids without RDF Mapping present.", - ['@ids' => $ids]); - $this->logger->warning("@msg", - [ - '@msg' => $msg, - ]); - throw new \Exception($msg); - } - } - else { - $data = $cache->data; - } - return $data; - } - - /** - * {@inheritdoc} - */ - public function generateContext(RdfMappingInterface $rdfMapping) { - // TODO: we will need to use \Drupal\Core\Field\FieldDefinitionInterface - // a lot to be able to create/frame/discern drupal bundles based on JSON-LD - // So keep an eye on that definition. - $allRdfNameSpaces = rdf_get_namespaces(); - - // This one will become our return value. - $jsonLdContextArray['@context'] = []; - - // Temporary array to keep track of our used namespaces and props. - $theAccumulator = []; - - $bundle_rdf_mappings = $rdfMapping->getPreparedBundleMapping(); - $drupal_types = $this->entityBundleIdsSplitter($rdfMapping->id()); - $entity_type_id = $drupal_types['entityTypeId']; - $bundle = $drupal_types['bundleId']; - // If we don't have rdf:type(s) for this bundle then it makes little - // sense to continue. - // This only generates an Exception if there is an - // rdfmapping object but has no rdf:type. - if (empty($bundle_rdf_mappings['types'])) { - $msg = t("Can't generate JSON-LD Context without at least one rdf:type for Entity type @entity_type, Bundle @bundle_name combo.", - ['@entity_type' => $entity_type_id, ' @bundle_name' => $bundle]); - $this->logger->warning("@msg", - [ - '@msg' => $msg, - ]); - throw new \Exception($msg); - } - - /* We have a lot of assumptions here (rdf module is strange) - a) xsd and other utility namespaces are in place - b) the user knows what/how rdf mapping works and does it right - c) that if a field's mapping_type is "rel" or "rev" and datatype is - not defined, then '@type' is uncertain. - d) that mapping back and forward is 1 to 1. - Drupal allows multiple fields to be mapped to a same rdf prop - but that does not scale back. If drupal gets an input with a list - of values for a given property, we would never know in which Drupal - fields we should put those values. it's the many to one, - one to many reduction problem made worst by the abstraction of - fields being containers of mappings and not rdf properties. */ - // Only care for those mappings that point to bundled or base fields. - // First our bundled fields. - foreach ($this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle) as $bundleFieldName => $fieldDefinition) { - $field_context = $this->getFieldsRdf($rdfMapping, $bundleFieldName, $fieldDefinition, $allRdfNameSpaces); - $theAccumulator = array_merge($field_context, $theAccumulator); - } - // And then our Base fields. - foreach ($this->entityFieldManager->getBaseFieldDefinitions($entity_type_id) as $baseFieldName => $fieldDefinition) { - $field_context = $this->getFieldsRdf($rdfMapping, $baseFieldName, $fieldDefinition, $allRdfNameSpaces); - $theAccumulator = array_merge($field_context, $theAccumulator); - } - $theAccumulator = array_filter($theAccumulator); - $jsonLdContextArray['@context'] = $theAccumulator; - return json_encode($jsonLdContextArray, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - } - - /** - * Gets the correct piece of @context for a given entity field. - * - * @param \Drupal\rdf\RdfMappingInterface $rdfMapping - * Rdf mapping object. - * @param string $field_name - * The name of the field. - * @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition - * The definition of the field. - * @param array $allRdfNameSpaces - * Every RDF prefixed namespace in this Drupal. - * - * @return array - * Piece of JSON-LD context that supports this field - */ - private function getFieldsRdf(RdfMappingInterface $rdfMapping, $field_name, FieldDefinitionInterface $fieldDefinition, array $allRdfNameSpaces) { - $termDefinition = []; - $fieldContextFragment = []; - $fieldRDFMapping = $rdfMapping->getPreparedFieldMapping($field_name); - if (!empty($fieldRDFMapping)) { - // If one ore more properties, all will share same datatype so - // get that before iterating. - // First get our defaults, no-user or config based input. - $default_field_term_mapping = $this->getTermContextFromField($fieldDefinition->getType()); - - // Now we start overriding from config entity defined mappings. - // Assume all non defined mapping types as "property". - $reltype = isset($fieldRDFMapping['mapping_type']) ? $fieldRDFMapping['mapping_type'] : 'property'; - - if (isset($fieldRDFMapping['datatype']) && ($reltype == 'property')) { - $termDefinition = ['@type' => $fieldRDFMapping['datatype']]; - } - if (!isset($fieldRDFMapping['datatype']) && ($reltype != 'property')) { - $termDefinition = ['@type' => '@id']; - } - - // This should respect user provided mapping and fill rest with defaults. - $termDefinition = $termDefinition + $default_field_term_mapping; - - // Now iterate over all properties for this field - // trying to parse them as compact IRI. - foreach ($fieldRDFMapping['properties'] as $property) { - $compactedDefinition = $this->parseCompactedIri($property); - if ($compactedDefinition['prefix'] != NULL) { - // Check if the namespace prefix exists. - if (array_key_exists($compactedDefinition['prefix'], $allRdfNameSpaces)) { - // Just overwrite as many times as needed, - // still faster than checking if - // it's there in the first place. - $fieldContextFragment[$compactedDefinition['prefix']] = $allRdfNameSpaces[$compactedDefinition['prefix']]; - $fieldContextFragment[$property] = $termDefinition; - } - } - } - } - - return $fieldContextFragment; - } - - /** - * Writes JSON-LD @context cache per Entity_type bundle combo. - * - * @param \Drupal\rdf\RdfMappingInterface $rdfMapping - * Rdf mapping object. - * @param string $cid - * Name of the cache bin to use. - * - * @return string - * A json encoded string for the processed JSON-LD @context - */ - protected function writeCache(RdfMappingInterface $rdfMapping, $cid) { - - // This is how an empty json encoded @context looks like. - $data = json_encode(['@context' => ''], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - try { - $data = $this->generateContext($rdfMapping); - $this->cache->set($cid, $data, Cache::PERMANENT, $rdfMapping->getCacheTagsToInvalidate()); - } - catch (\Exception $e) { - $this->logger->warning("@msg", - [ - '@msg' => $e->getMessage(), - ]); - } - - return $data; - } - - /** - * Absurdly simple exploder for a joint entityType and Bundle ids string. - * - * @param string $ids - * A string with containing entity id and bundle joined by a dot. - * - * @return array - * And array with the entity type and the bundle id - */ - protected function entityBundleIdsSplitter($ids) { - list($entity_type_id, $bundle_id) = explode(".", $ids, 2); - return ['entityTypeId' => $entity_type_id, 'bundleId' => $bundle_id]; - } - - /** - * Parses and IRI, checks if it is complaint with compacted IRI definition. - * - * Assumes this notion of compact IRI/similar to CURIE - * http://json-ld.org/spec/ED/json-ld-syntax/20120522/#dfn-prefix. - * - * @param string $iri - * IRIs are strings. - * - * @return array - * If $iri is a compacted iri, prefix and term as separate - * array members, if not, unmodified $iri in term position - * and null prefix. - */ - protected function parseCompactedIri($iri) { - // As naive as it gets. - list($prefix, $rest) = array_pad(explode(":", $iri, 2), 2, ''); - if ((substr($rest, 0, 2) == "//") || ($prefix == $iri)) { - // Means this was never a compacted IRI. - return ['prefix' => NULL, 'term' => $iri]; - } - return ['prefix' => $prefix, 'term' => $rest]; - } - - /** - * Naive approach on Drupal field to JSON-LD type mapping. - * - * TODO: Would be fine to have this definitions in an - * configEntity way in the future. - * - * @param string $field_type - * As provided by \Drupal\Core\Field\FieldDefinitionInterface::getType(). - * - * @return array - * A json-ld term definition if there is a match - * or array("@type" => "xsd:string") in case of no match. - */ - protected function getTermContextFromField($field_type) { - // Be aware that drupal field definitions can be complex. - // e.g text_with_summary has a text, a summary, a number of lines, etc - // we are only dealing with the resulting ->value() of all this separate - // pieces and mapping only that as a whole. - // Default mapping to return in case no $field_type matches - // field_mappings array keys. - $default_mapping = [ - "@type" => "xsd:string", - ]; - - $field_mappings = [ - "comment" => [ - "@type" => "xsd:string", - ], - "datetime" => [ - "@type" => "xsd:dateTime", - ], - "file" => [ - "@type" => "@id", - ], - "image" => [ - "@type" => "@id", - ], - "link" => [ - "@type" => "xsd:anyURI", - ], - "list_float" => [ - "@type" => "xsd:float", - "@container" => "@list", - ], - "list_integer" => [ - "@type" => "xsd:int", - "@container" => "@list", - ], - "list_string" => [ - "@type" => "xsd:string", - "@container" => "@list", - ], - "path" => [ - "@type" => "xsd:anyURI", - ], - "text" => [ - "@type" => "xsd:string", - ], - "text_with_summary" => [ - "@type" => "xsd:string", - ], - "text_long" => [ - "@type" => "xsd:string", - ], - "uuid" => [ - "@type" => "xsd:string", - ], - "uri" => [ - "@type" => "xsd:anyURI", - ], - "language" => [ - "@type" => "xsd:language", - ], - "string_long" => [ - "@type" => "xsd:string", - ], - "changed" => [ - "@type" => "xsd:dateTime", - ], - "map" => "xsd:", - "boolean" => [ - "@type" => "xsd:boolean", - ], - "email" => [ - "@type" => "xsd:string", - ], - "integer" => [ - "@type" => "xsd:int", - ], - "decimal" => [ - "@type" => "xsd:decimal", - ], - "created" => [ - "@type" => "xsd:dateTime", - ], - "float" => [ - "@type" => "xsd:float", - ], - "entity_reference" => [ - "@type" => "@id", - ], - "timestamp" => [ - "@type" => "xsd:dateTime", - ], - "string" => [ - "@type" => "xsd:string", - ], - "password" => [ - "@type" => "xsd:string", - ], - ]; - - return array_key_exists($field_type, $field_mappings) ? $field_mappings[$field_type] : $default_mapping; - - } - -} diff --git a/src/JsonldContextGenerator/JsonldContextGeneratorInterface.php b/src/JsonldContextGenerator/JsonldContextGeneratorInterface.php deleted file mode 100644 index 90e22768..00000000 --- a/src/JsonldContextGenerator/JsonldContextGeneratorInterface.php +++ /dev/null @@ -1,44 +0,0 @@ - ['schema:dateCreated'], - 'datatype' => 'xsd:dateTime', - 'datatype_callback' => ['callable' => 'Drupal\rdf\CommonDataConverter::dateIso8601Value'], - ]; - - // Save bundle mapping config. - $rdfMapping = rdf_get_mapping('entity_test', 'rdf_source') - ->setBundleMapping(['types' => $types]) - ->setFieldMapping('created', $mapping) - ->save(); - // Initialize our generator. - $this->theJsonldContextGenerator = new JsonldContextGenerator( - $this->container->get('entity_field.manager'), - $this->container->get('entity_type.bundle.info'), - $this->container->get('entity_type.manager'), - $this->container->get('cache.default'), - $this->container->get('logger.channel.islandora') - ); - - } - - /** - * @covers \Drupal\islandora\JsonldContextGenerator\JsonldContextGenerator::getContext - */ - public function testGetContext() { - // Test with known asserts. - $context = $this->theJsonldContextGenerator->getContext('entity_test.rdf_source'); - $context_as_array = json_decode($context, TRUE); - $this->assertTrue(is_array($context_as_array), 'JSON-LD Context generated has correct structure for known Bundle'); - - $this->assertTrue(strpos($context, '"schema": "http://schema.org/"') !== FALSE, "JSON-LD Context generated contains the expected values for known Bundle"); - - } - - /** - * Tests Exception in case of no rdf type. - * - * @expectedException \Exception - * @covers \Drupal\islandora\JsonldContextGenerator\JsonldContextGenerator::getContext - */ - public function testGetContextException() { - // This should throw the expected Exception. - $newFedoraEntity = $this->drupalCreateFedoraContentType(); - $this->theJsonldContextGenerator->getContext('fedora_resource.' . $newFedoraEntity->id()); - - } - - /** - * @covers \Drupal\islandora\JsonldContextGenerator\JsonldContextGenerator::generateContext - */ - public function testGenerateContext() { - // Test with known asserts. - $rdfMapping = rdf_get_mapping('entity_test', 'rdf_source'); - $context = $this->theJsonldContextGenerator->generateContext($rdfMapping); - $context_as_array = json_decode($context, TRUE); - $this->assertTrue(is_array($context_as_array), 'JSON-LD Context generated has correct structure for known Bundle'); - - $this->assertTrue(strpos($context, '"schema": "http://schema.org/"') !== FALSE, "JSON-LD Context generated contains the expected values for known Bundle"); - - } - - /** - * Tests Exception in case of no rdf type. - * - * @expectedException \Exception - * @covers \Drupal\islandora\JsonldContextGenerator\JsonldContextGenerator::generateContext - */ - public function testGenerateContextException() { - // This should throw the expected Exception. - $newFedoraEntity = $this->drupalCreateFedoraContentType(); - $rdfMapping = rdf_get_mapping('fedora_resource', $newFedoraEntity->id()); - $this->theJsonldContextGenerator->getContext('fedora_resource.' . $newFedoraEntity->id()); - - } - -}