Browse Source

Remove Jsonld Context Generator (#60)

pull/756/head
Jared Whiklo 7 years ago committed by dannylamb
parent
commit
7222f8a53d
  1. 3
      islandora.services.yml
  2. 10
      src/Controller/FedoraResourceJsonLdContextController.php
  3. 424
      src/JsonldContextGenerator/JsonldContextGenerator.php
  4. 44
      src/JsonldContextGenerator/JsonldContextGeneratorInterface.php
  5. 132
      tests/src/Kernel/JsonldContextGeneratorTest.php

3
islandora.services.yml

@ -24,6 +24,3 @@ services:
logger.channel.islandora: logger.channel.islandora:
parent: logger.channel_base parent: logger.channel_base
arguments: ['islandora'] 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']

10
src/Controller/FedoraResourceJsonLdContextController.php

@ -4,7 +4,7 @@ namespace Drupal\islandora\Controller;
use Drupal\Core\Cache\CacheableJsonResponse; use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\islandora\JsonldContextGenerator\JsonldContextGeneratorInterface; use Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -22,14 +22,14 @@ class FedoraResourceJsonLdContextController extends ControllerBase {
/** /**
* Injected JsonldContextGenerator. * Injected JsonldContextGenerator.
* *
* @var \Drupal\islandora\JsonldContextGenerator\JsonldContextGeneratorInterface * @var \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface
*/ */
private $jsonldContextGenerator; private $jsonldContextGenerator;
/** /**
* FedoraResourceJsonLdContextController constructor. * FedoraResourceJsonLdContextController constructor.
* *
* @param \Drupal\islandora\JsonldContextGenerator\JsonldContextGeneratorInterface $jsonld_context_generator * @param \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface $jsonld_context_generator
* Injected JsonldContextGenerator. * Injected JsonldContextGenerator.
*/ */
public function __construct(JsonldContextGeneratorInterface $jsonld_context_generator) { public function __construct(JsonldContextGeneratorInterface $jsonld_context_generator) {
@ -43,10 +43,10 @@ class FedoraResourceJsonLdContextController extends ControllerBase {
* The App Container. * The App Container.
* *
* @return static * @return static
* An instance of our islandora.jsonldcontextgenerator service. * An instance of our jsonld.contextgenerator service.
*/ */
public static function create(ContainerInterface $container) { public static function create(ContainerInterface $container) {
return new static($container->get('islandora.jsonldcontextgenerator')); return new static($container->get('jsonld.contextgenerator'));
} }
/** /**

424
src/JsonldContextGenerator/JsonldContextGenerator.php

@ -1,424 +0,0 @@
<?php
namespace Drupal\islandora\JsonldContextGenerator;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\rdf\RdfMappingInterface;
use Drupal\rdf\Entity\RdfMapping;
use Psr\Log\LoggerInterface;
/**
* A reliable JSON-LD @Context generation class.
*
* Class JsonldContextGenerator.
*
* @package Drupal\islandora\JsonldContextGenerator
*/
class JsonldContextGenerator implements JsonldContextGeneratorInterface {
/**
* Constant Naming convention used to prefix name cache bins($cid)
*/
const CACHE_BASE_CID = 'islandora:jsonld:context';
/**
* Injected EntityFieldManager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager = NULL;
/**
* Injected EntityTypeManager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager = NULL;
/**
* Injected EntityTypeBundle.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo = NULL;
/**
* Injected Cache implementation.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Injected Logger Interface.
*
* @var \Psr\Log\LoggerInterface
* A logger instance.
*/
protected $logger;
/**
* Constructs a JsonldContextGenerator object.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
* The Entity Type Manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Caching Backend.
* @param \Psr\Log\LoggerInterface $logger_channel
* Our Logging Channel.
*/
public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeBundleInfoInterface $bundle_info, EntityTypeManagerInterface $entity_manager, CacheBackendInterface $cache_backend, LoggerInterface $logger_channel) {
$this->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;
}
}

44
src/JsonldContextGenerator/JsonldContextGeneratorInterface.php

@ -1,44 +0,0 @@
<?php
namespace Drupal\islandora\JsonldContextGenerator;
use Drupal\rdf\RdfMappingInterface;
/**
* Interface for a service that provides per Bundle JSON-LD Context generation.
*
* @ingroup: islandora
*/
interface JsonldContextGeneratorInterface {
/**
* Generates an JSON-LD Context string based on an RdfMapping object.
*
* @param \Drupal\rdf\Entity\RdfMapping|RdfMappingInterface $mapping
* An RDF Mapping Object.
*
* @return string
* A JSON-LD @context as string.
*
* @throws \Exception
* If no RDF mapping has no rdf:type assigned.
*/
public function generateContext(RdfMappingInterface $mapping);
/**
* Returns an JSON-LD Context string.
*
* This method should be invoked if caching and speed is required.
*
* @param string $ids
* In the form of "entity_type.bundle_name".
*
* @return string
* A JSON-LD @context as string.
*
* @throws \Exception
* If no RDF mapping exists.
*/
public function getContext($ids);
}

132
tests/src/Kernel/JsonldContextGeneratorTest.php

@ -1,132 +0,0 @@
<?php
namespace Drupal\Tests\islandora\Kernel;
use Drupal\islandora\JsonldContextGenerator\JsonldContextGenerator;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the Json-LD context Generator methods and simple integration.
*
* @group islandora
* @coversDefaultClass \Drupal\islandora\JsonldContextGenerator\JsonldContextGenerator
*/
class JsonldContextGeneratorTest extends KernelTestBase {
use FedoraContentTypeCreationTrait {
createFedoraResourceContentType as drupalCreateFedoraContentType;
}
public static $modules = [
'system',
'rdf',
'islandora',
'entity_test',
'rdf_test_namespaces',
];
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityBundleListenerInterface
*/
protected $entityBundleListener;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The JsonldContextGenerator we are testing.
*
* @var \Drupal\islandora\JsonldContextGenerator\JsonldContextGeneratorInterface
*/
protected $theJsonldContextGenerator;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$types = ['schema:Thing'];
$mapping = [
'properties' => ['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());
}
}
Loading…
Cancel
Save