diff --git a/rdf_entity.links.task.yml b/rdf_entity.links.task.yml
index a782c7e0..0f3a89f8 100755
--- a/rdf_entity.links.task.yml
+++ b/rdf_entity.links.task.yml
@@ -25,3 +25,9 @@ entity.rdf_entity.collection:
title: RDF
route_name: entity.rdf_entity.collection
base_route: system.admin_content
+
+entity.rdf_entity.version_history:
+ route_name: entity.rdf_entity.version_history
+ base_route: entity.rdf_entity.canonical
+ title: 'Revisions'
+ weight: 20
\ No newline at end of file
diff --git a/rdf_entity.routing.yml b/rdf_entity.routing.yml
index 748b1fba..2266b8b0 100755
--- a/rdf_entity.routing.yml
+++ b/rdf_entity.routing.yml
@@ -11,6 +11,18 @@ entity.rdf_entity.canonical:
# Calls the access controller of the entity, $operation 'view'.
_entity_access: 'rdf_entity.view'
+entity.rdf_entity.version_history:
+ path: '/rdf_entity/{rdf_entity}/revisions'
+ defaults:
+ _title: 'Revisions'
+ _controller: '\Drupal\rdf_entity\Controller\RdfController::revisionOverview'
+ requirements:
+ _entity_access: 'rdf_entity.edit'
+ options:
+ parameters:
+ rdf_entity:
+ type: entity:rdf_entity
+
entity.rdf_entity.collection:
path: 'admin/content/rdf'
defaults:
@@ -148,3 +160,54 @@ entity.rdf_entity_graph.disable:
toggle_operation: disable
requirements:
_custom_access: 'Drupal\rdf_entity\Controller\RdfEntityGraphToggle::access'
+
+entity.rdf_entity.revision:
+ path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/view'
+ defaults:
+ _controller: 'Drupal\rdf_entity\Controller\RdfController::revisionShow'
+ options:
+ parameters:
+ rdf_entity:
+ type: entity:rdf_entity
+ rdf_revision:
+ type: entity:rdf_entity
+ requirements:
+ # Calls the access controller of the entity, $operation 'view'.
+ _entity_access: 'rdf_entity.view'
+
+rdf_entity.revision_revert_confirm:
+ path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/revert'
+ defaults:
+ _form: '\Drupal\rdf_entity\Form\RdfRevisionRevertForm'
+ _title: 'Revert to earlier revision'
+ options:
+ parameters:
+ rdf_entity:
+ type: entity:rdf_entity
+ rdf_revision:
+ type: entity:rdf_entity
+ requirements:
+ _entity_access: 'rdf_entity.edit'
+# T
+rdf_entity.revision_revert_translation_confirm:
+ path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/revert/{langcode}'
+ defaults:
+ _form: '\Drupal\rdf_entity\Form\RdfRevisionRevertTranslationForm'
+ _title: 'Revert to earlier revision of a translation'
+ parameters:
+ rdf_entity:
+ type: entity:rdf_entity
+ rdf_revision:
+ type: entity:rdf_entity
+
+rdf_entity.revision_delete_confirm:
+ path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/delete'
+ defaults:
+ _form: '\Drupal\rdf_entity\Form\RdfRevisionDeleteForm'
+ _title: 'Delete earlier revision'
+ options:
+ parameters:
+ rdf_entity:
+ type: entity:rdf_entity
+ rdf_revision:
+ type: entity:rdf_entity
\ No newline at end of file
diff --git a/rdf_entity.services.yml b/rdf_entity.services.yml
index 7b36dfb1..cde3c9c6 100644
--- a/rdf_entity.services.yml
+++ b/rdf_entity.services.yml
@@ -55,3 +55,6 @@ services:
arguments: ['@typed_data_manager']
tags:
- { name: event_subscriber }
+ rdf_entity.graph_priority.filter:
+ class: Drupal\rdf_entity\LayeredGraphPriorityFilter
+ arguments: ['@entity_type.manager']
\ No newline at end of file
diff --git a/src/Controller/RdfController.php b/src/Controller/RdfController.php
index f089ec37..48f1d547 100644
--- a/src/Controller/RdfController.php
+++ b/src/Controller/RdfController.php
@@ -4,13 +4,57 @@
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\Controller\EntityViewController;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Url;
+use Drupal\rdf_entity\RdfEntitySparqlStorageInterface;
use Drupal\rdf_entity\RdfEntityTypeInterface;
use Drupal\rdf_entity\RdfInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides route responses for rdf_entity.module.
*/
-class RdfController extends ControllerBase {
+class RdfController extends ControllerBase implements ContainerInjectionInterface {
+
+ /**
+ * The date formatter service.
+ *
+ * @var \Drupal\Core\Datetime\DateFormatterInterface
+ */
+ protected $dateFormatter;
+
+ /**
+ * The renderer service.
+ *
+ * @var \Drupal\Core\Render\RendererInterface
+ */
+ protected $renderer;
+
+ /**
+ * Constructs a RdfController object.
+ *
+ * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+ * The date formatter service.
+ * @param \Drupal\Core\Render\RendererInterface $renderer
+ * The renderer service.
+ */
+ public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer) {
+ $this->dateFormatter = $date_formatter;
+ $this->renderer = $renderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('date.formatter'),
+ $container->get('renderer')
+ );
+ }
/**
* Route title callback.
@@ -108,4 +152,183 @@ public function addPage() {
return $build;
}
+ /**
+ * Generates an overview table of older revisions of a entity.
+ *
+ * @param \Drupal\rdf_entity\RdfInterface $rdf_entity
+ * A rdf object.
+ *
+ * @return array
+ * An array as expected by \Drupal\Core\Render\RendererInterface::render().
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ public function revisionOverview(RdfInterface $rdf_entity) {
+ $account = $this->currentUser();
+ $langcode = $rdf_entity->language()->getId();
+ $langname = $rdf_entity->language()->getName();
+ $languages = $rdf_entity->getTranslationLanguages();
+ $has_translations = (count($languages) > 1);
+ $rdf_storage = $this->entityManager()->getStorage('rdf_entity');
+ $type = $rdf_entity->getType();
+
+ $build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $rdf_entity->label()]) : $this->t('Revisions for %title', ['%title' => $rdf_entity->label()]);
+ $header = [$this->t('Revision'), $this->t('Operations')];
+
+ $revert_permission = (($account->hasPermission("revert $type revisions") || $account->hasPermission('revert all revisions') || $account->hasPermission('administer nodes')) && $rdf_entity->access('update'));
+ $delete_permission = (($account->hasPermission("delete $type revisions") || $account->hasPermission('delete all revisions') || $account->hasPermission('administer nodes')) && $rdf_entity->access('delete'));
+
+ $rows = [];
+ $default_revision = $rdf_entity->getRevisionId();
+ $current_revision_displayed = FALSE;
+
+ $revisions = $this->getRevisionIds($rdf_entity, $rdf_storage);
+
+ foreach ($revisions as $vid) {
+ /** @var \Drupal\rdf_entity\RdfInterface $revision */
+ $revision = $rdf_storage->loadRevision($vid);
+ // Only show revisions that are affected by the language that is being
+ // displayed.
+ // @todo && $revision->getTranslation($langcode)->isRevisionTranslationAffected()
+ if ($revision->hasTranslation($langcode) ) {
+ $username = [
+ '#theme' => 'username',
+ '#account' => $revision->getRevisionUser(),
+ ];
+
+ // Use revision link to link to revisions that are not active.
+ $date = $this->dateFormatter->format($revision->getChangedTime(), 'short');
+
+ // We treat also the latest translation-affecting revision as current
+ // revision, if it was the default revision, as its values for the
+ // current language will be the same of the current default revision in
+ // this case.
+ $is_current_revision = $vid == $default_revision || (!$current_revision_displayed && $revision->wasDefaultRevision());
+ if (!$is_current_revision) {
+ $link = $this->l($date, new Url('entity.rdf_entity.revision', ['rdf_entity' => $rdf_entity->id(), 'rdf_revision' => $vid]));
+ }
+ else {
+ $link = $rdf_entity->link($date);
+ $current_revision_displayed = TRUE;
+ }
+
+ $row = [];
+ $column = [
+ 'data' => [
+ '#type' => 'inline_template',
+ '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}
{{ message }}
{% endif %}',
+ '#context' => [
+ 'date' => $link,
+ 'username' => $this->renderer->renderPlain($username),
+ 'message' => ['#markup' => $revision->revision_log->value, '#allowed_tags' => Xss::getHtmlTagList()],
+ ],
+ ],
+ ];
+ // @todo Simplify once https://www.drupal.org/node/2334319 lands.
+ $this->renderer->addCacheableDependency($column['data'], $username);
+ $row[] = $column;
+
+ if ($is_current_revision) {
+ $row[] = [
+ 'data' => [
+ '#prefix' => '',
+ '#markup' => $this->t('Current revision'),
+ '#suffix' => '',
+ ],
+ ];
+
+ $rows[] = [
+ 'data' => $row,
+ 'class' => ['revision-current'],
+ ];
+ }
+ else {
+ $links = [];
+ if ($revert_permission) {
+ $links['revert'] = [
+ 'title' => $vid < $rdf_entity->getRevisionId() ? $this->t('Revert') : $this->t('Set as current revision'),
+ 'url' => $has_translations ?
+ Url::fromRoute('rdf_entity.revision_revert_translation_confirm', ['rdf_rntity' => $rdf_entity->id(), 'rdf_revision' => $vid, 'langcode' => $langcode]) :
+ Url::fromRoute('rdf_entity.revision_revert_confirm', ['rdf_entity' => $rdf_entity->id(), 'rdf_revision' => $vid]),
+ ];
+ }
+
+ if ($delete_permission) {
+ $links['delete'] = [
+ 'title' => $this->t('Delete'),
+ 'url' => Url::fromRoute('rdf_entity.revision_delete_confirm', ['rdf_entity' => $rdf_entity->id(), 'rdf_revision' => $vid]),
+ ];
+ }
+
+ $row[] = [
+ 'data' => [
+ '#type' => 'operations',
+ '#links' => $links,
+ ],
+ ];
+
+ $rows[] = $row;
+ }
+ }
+ }
+
+ $build['node_revisions_table'] = [
+ '#theme' => 'table',
+ '#rows' => $rows,
+ '#header' => $header,
+ '#attached' => [
+ 'library' => ['node/drupal.node.admin'],
+ ],
+ '#attributes' => ['class' => 'node-revision-table'],
+ ];
+
+ $build['pager'] = ['#type' => 'pager'];
+
+ // Bypass cache for development
+ $build['#cache']['max-age'] = 0;
+
+ return $build;
+ }
+
+ /**
+ * Gets a list of rdf revision IDs for a specific entity.
+ *
+ * @param \Drupal\rdf_entity\RdfInterface $rdf_entity
+ * The rdf entity.
+ * @param \Drupal\rdf_entity\RdfEntitySparqlStorageInterface $rdf_storage
+ * The rdf storage handler.
+ *
+ * @return int[]
+ * Node revision IDs (in descending order).
+ */
+ protected function getRevisionIds(RdfInterface $rdf_entity, RdfEntitySparqlStorageInterface $rdf_storage) {
+ $query = $rdf_storage->getQuery()
+ ->allRevisions()
+ //->condition($rdf_entity->getEntityType()->getKey('bundle'), 'event')
+ ->condition($rdf_entity->getEntityType()->getKey('id'), $rdf_entity->id())
+ ->sort($rdf_entity->getEntityType()->getKey('revision'), 'DESC')
+ ->pager(50);
+ $result= $query->execute();
+ return array_keys($result);
+ }
+
+
+ /**
+ * Displays a rdf revision.
+ *
+ * @param \Drupal\rdf_entity\RdfInterface $rdf_revision
+ * The rdf revision.
+ *
+ * @return array
+ * An array suitable for \Drupal\Core\Render\RendererInterface::render().
+ */
+ public function revisionShow(RdfInterface $rdf_revision) {
+ $rdf_entity = $this->entityManager()->getTranslationFromContext($rdf_revision);
+ $view_controller = new EntityViewController($this->entityManager, $this->renderer);
+ $page = $view_controller->view($rdf_entity);
+ unset($page['#cache']);
+ return $page;
+ }
+
}
diff --git a/src/Entity/Query/Sparql/Query.php b/src/Entity/Query/Sparql/Query.php
index 32939fd9..12c25f3f 100644
--- a/src/Entity/Query/Sparql/Query.php
+++ b/src/Entity/Query/Sparql/Query.php
@@ -352,6 +352,14 @@ protected function conditionGroupFactory($conjunction = 'AND') {
return new $class($conjunction, $this, $this->namespaces, $this->graphHandler, $this->fieldHandler);
}
+ public function getLatestRevision() {
+ return $this->latestRevision;
+ }
+
+ public function getAllRevisions() {
+ return $this->allRevisions;
+ }
+
/**
* Return the query string for debugging help.
*
diff --git a/src/Entity/Query/Sparql/SparqlCondition.php b/src/Entity/Query/Sparql/SparqlCondition.php
index 13acedd8..bc2e4c12 100644
--- a/src/Entity/Query/Sparql/SparqlCondition.php
+++ b/src/Entity/Query/Sparql/SparqlCondition.php
@@ -295,6 +295,16 @@ public function condition($field = NULL, $value = NULL, $operator = NULL, $lang
* Thrown if the value is NULL or the operator is not allowed.
*/
public function keyCondition($field, $value, $operator) {
+ // Revision handling.
+ $query = $this->query;
+ if ($query instanceof SparqlQueryInterface) {
+ if ($field === 'id' && $query->getAllRevisions()) {
+ $field = 'vid';
+ }
+ }
+
+
+
// @todo: Add support for loadMultiple with empty Id (load all).
if ($value == NULL) {
throw new \Exception('The value cannot be NULL for conditions related to the Id and bundle keys.');
diff --git a/src/Entity/Query/Sparql/SparqlQueryInterface.php b/src/Entity/Query/Sparql/SparqlQueryInterface.php
index 12913688..129a46e1 100644
--- a/src/Entity/Query/Sparql/SparqlQueryInterface.php
+++ b/src/Entity/Query/Sparql/SparqlQueryInterface.php
@@ -44,4 +44,7 @@ public function getEntityType(): EntityTypeInterface;
*/
public function getEntityStorage(): RdfEntitySparqlStorageInterface;
+ public function getLatestRevision();
+
+ public function getAllRevisions();
}
diff --git a/src/Entity/Rdf.php b/src/Entity/Rdf.php
index aaa4c6e2..8a8d491f 100755
--- a/src/Entity/Rdf.php
+++ b/src/Entity/Rdf.php
@@ -3,6 +3,7 @@
namespace Drupal\rdf_entity\Entity;
use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\RevisionLogEntityTrait;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
@@ -32,25 +33,35 @@
* },
* "access" = "Drupal\rdf_entity\RdfAccessControlHandler",
* },
+ * show_revision_ui = TRUE,
* list_cache_contexts = { "user" },
* base_table = null,
+ * revision_table = null,
* admin_permission = "administer rdf entity",
* fieldable = TRUE,
* translatable = TRUE,
* entity_keys = {
* "id" = "id",
+ * "revision" = "vid",
* "uid" = "uid",
* "bundle" = "rid",
* "langcode" = "langcode",
* "label" = "label",
* "uuid" = "uuid",
* },
+ * revision_metadata_keys = {
+ * "revision_user" = "revision_uid",
+ * "revision_created" = "revision_timestamp",
+ * "revision_log_message" = "revision_log"
+ * },
* bundle_entity_type = "rdf_type",
* links = {
* "canonical" = "/rdf_entity/{rdf_entity}",
* "edit-form" = "/rdf_entity/{rdf_entity}/edit",
* "delete-form" = "/rdf_entity/{rdf_entity}/delete",
- * "collection" = "/rdf_entity/list"
+ * "collection" = "/rdf_entity/list",
+ * "version-history" = "/rdf_entity/{rdf_entity}/revisions",
+ * "revision" = "/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/view",
* },
* field_ui_base_route = "entity.rdf_type.edit_form",
* permission_granularity = "bundle",
@@ -60,6 +71,7 @@
class Rdf extends ContentEntityBase implements RdfInterface {
use EntityChangedTrait;
+ use RevisionLogEntityTrait;
/**
* Entity bundle.
@@ -68,6 +80,8 @@ class Rdf extends ContentEntityBase implements RdfInterface {
*/
protected $rid;
+ protected $vid;
+
/**
* {@inheritdoc}
*
@@ -140,6 +154,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setLabel(t('ID'))
->setTranslatable(FALSE);
+ if ($entity_type->hasKey('revision')) {
+ $fields[$entity_type->getKey('revision')] = BaseFieldDefinition::create('uri')
+ ->setLabel(new TranslatableMarkup('Revision ID'))
+ ->setReadOnly(TRUE);
+ }
+
$fields['rid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Rdf Type'))
->setDescription(t('The Rdf type of this entity.'))
@@ -212,6 +232,9 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
'weight' => 2,
]);
+ // Add the revision metadata fields.
+ $fields += static::revisionLogBaseFieldDefinitions($entity_type);
+
return $fields;
}
diff --git a/src/Entity/RdfEntitySparqlStorage.php b/src/Entity/RdfEntitySparqlStorage.php
index c3d14b65..87cb33b5 100644
--- a/src/Entity/RdfEntitySparqlStorage.php
+++ b/src/Entity/RdfEntitySparqlStorage.php
@@ -21,10 +21,16 @@
use Drupal\rdf_entity\Database\Driver\sparql\ConnectionInterface;
use Drupal\rdf_entity\Entity\Query\Sparql\SparqlArg;
use Drupal\rdf_entity\Exception\DuplicatedIdException;
+use Drupal\rdf_entity\HydratedEntity;
+use Drupal\rdf_entity\HydratedEntityList;
use Drupal\rdf_entity\RdfEntityIdPluginManager;
use Drupal\rdf_entity\RdfEntitySparqlStorageInterface;
use Drupal\rdf_entity\RdfFieldHandlerInterface;
use Drupal\rdf_entity\RdfGraphHandlerInterface;
+use Drupal\rdf_entity\RdfInterface;
+use Drupal\rdf_entity\RawEntity;
+use Drupal\rdf_entity\LayeredGraphPriorityFilter;
+use Drupal\rdf_entity\RawEntityRepository;
use EasyRdf\Graph;
use EasyRdf\Literal;
use EasyRdf\Sparql\Result;
@@ -97,6 +103,8 @@ class RdfEntitySparqlStorage extends ContentEntityStorageBase implements RdfEnti
*/
protected $entityIdPluginManager;
+ protected $graphPriorityFilter;
+
/**
* Initialize the storage backend.
*
@@ -120,11 +128,14 @@ class RdfEntitySparqlStorage extends ContentEntityStorageBase implements RdfEnti
* The rdf mapping helper service.
* @param \Drupal\rdf_entity\RdfEntityIdPluginManager $entity_id_plugin_manager
* The RDF entity ID generator plugin manager.
+ * @param \Drupal\rdf_entity\LayeredGraphPriorityFilter
+ * The entity entity repository filter the
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
* The memory cache backend.
*/
- public function __construct(EntityTypeInterface $entity_type, ConnectionInterface $sparql, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, RdfGraphHandlerInterface $rdf_graph_handler, RdfFieldHandlerInterface $rdf_field_handler, RdfEntityIdPluginManager $entity_id_plugin_manager, MemoryCacheInterface $memory_cache = NULL) {
+ public function __construct(EntityTypeInterface $entity_type, ConnectionInterface $sparql, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, RdfGraphHandlerInterface $rdf_graph_handler, RdfFieldHandlerInterface $rdf_field_handler, RdfEntityIdPluginManager $entity_id_plugin_manager, LayeredGraphPriorityFilter $filter, MemoryCacheInterface $memory_cache = NULL) {
parent::__construct($entity_type, $entity_manager, $cache, $memory_cache);
+ $this->revisionKey = $this->entityType->getKey('revision');
$this->sparql = $sparql;
$this->languageManager = $language_manager;
$this->entityTypeManager = $entity_type_manager;
@@ -132,6 +143,7 @@ public function __construct(EntityTypeInterface $entity_type, ConnectionInterfac
$this->graphHandler = $rdf_graph_handler;
$this->fieldHandler = $rdf_field_handler;
$this->entityIdPluginManager = $entity_id_plugin_manager;
+ $this->graphPriorityFilter = $filter;
}
/**
@@ -149,11 +161,45 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
$container->get('sparql.graph_handler'),
$container->get('sparql.field_handler'),
$container->get('plugin.manager.rdf_entity.id'),
+ $container->get('rdf_entity.graph_priority.filter'),
// We support also Drupal 8.5.x.
$container->has('entity.memory_cache') ? $container->get('entity.memory_cache') : NULL
);
}
+ /**
+ * Format the entity select SPARQL query.
+ *
+ * @todo: We should filter per entity per graph and not load the whole
+ * database only to filter later on.
+ * @see https://github.com/ec-europa/rdf_entity/issues/19
+ *
+ * @param array $ids
+ * @param array $graphs
+ *
+ * @return string
+ */
+ protected static function formatEntitySelectQuery(array $ids, array $graphs): string {
+ $ids_string = SparqlArg::serializeUris($ids, ' ');
+ $named_graph = '';
+ foreach ($graphs as $graph) {
+ $named_graph .= 'FROM NAMED ' . SparqlArg::uri($graph) . "\n";
+ }
+
+ // @todo https://github.com/ec-europa/rdf_entity/issues/19
+ $query = << $v) {
unset($remaining_ids[$k]);
}
- $entities_values = $this->loadFromStorage($operation_ids, $graph_ids);
- if ($entities_values) {
- foreach ($entities_values as $id => $entity_values) {
- $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
- $langcode_key = $this->getEntityType()->getKey('langcode');
- $translations = [];
- if (!empty($entities_values[$id][$langcode_key])) {
- foreach ($entities_values[$id][$langcode_key] as $langcode => $data) {
- if (!empty(reset($data)['value'])) {
- $translations[] = reset($data)['value'];
- }
- }
- }
- $entity = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, $translations);
- $this->trackOriginalGraph($entity);
- $entities[$id] = $entity;
- }
+ $entities = $this->loadFromStorage($operation_ids, $graph_ids);
+ if ($entities) {
$this->invokeStorageLoadHook($entities);
$this->setPersistentCache($entities);
}
@@ -276,33 +307,16 @@ protected function loadFromStorage(array $ids, array $graph_ids): ?array {
return [];
}
- // @todo: We should filter per entity per graph and not load the whole
- // database only to filter later on.
- // @see https://github.com/ec-europa/rdf_entity/issues/19
- $ids_string = SparqlArg::serializeUris($ids, ' ');
$graphs = $this->getGraphHandler()->getEntityTypeGraphUrisFlatList($this->getEntityTypeId());
- $named_graph = '';
- foreach ($graphs as $graph) {
- $named_graph .= 'FROM NAMED ' . SparqlArg::uri($graph) . "\n";
- }
+ $query = self::formatEntitySelectQuery($ids, $graphs);
- // @todo Get rid of the language filter. It's here because of eurovoc:
- // \Drupal\taxonomy\Form\OverviewTerms::buildForm loads full entities
- // of the whole tree: 7000+ terms in 24 languages is just too much.
- // @see https://github.com/ec-europa/rdf_entity/issues/19
- $query = <<sparql->query($query);
+ $entities = $this->processGraphResults($query_result, $graph_ids);
- $entity_values = $this->sparql->query($query);
- return $this->processGraphResults($entity_values, $graph_ids);
+ foreach ($entities as $id => $entity) {
+ $this->trackOriginalGraph($entity);
+ }
+ return $entities;
}
/**
@@ -327,123 +341,76 @@ protected function loadFromStorage(array $ids, array $graph_ids): ?array {
*
* @throws \Exception
* Thrown when the entity graph is empty.
+ */
+ protected function processGraphResults($results, array $graph_ids): array {
+ $entity_repo = RawEntityRepository::createFromResult($results);
+ // Filter down to one result per subject (graph with highest priority).
+ $filtered_result_set = $this->graphPriorityFilter->filter($entity_repo, $graph_ids, $this->getEntityTypeId());
+
+ return $this->hydrateRepository($filtered_result_set);
+ }
+
+ /**
+ * @param \Drupal\rdf_entity\RawEntity $raw_entity
*
- * @see https://github.com/ec-europa/rdf_entity/issues/19
- *
- * @todo Reduce the cyclomatic complexity of this function in #19.
+ * @return array
+ * @throws \Exception
*/
- protected function processGraphResults($results, array $graph_ids): ?array {
- $values_per_entity = $this->deserializeGraphResults($results);
- if (empty($values_per_entity)) {
+ protected function hydrateEntity(RawEntity $raw_entity): ?EntityInterface {
+ $entity_array = [];
+ $bundle = $this->getActiveBundle($raw_entity);
+ if (!$bundle) {
return NULL;
}
+ $entity_array = $this->attachEntityId($entity_array, $bundle, $raw_entity);
+ $entity_array = $this->attachBundle($bundle, $entity_array);
- $default_language = $this->languageManager->getDefaultLanguage()->getId();
- $inbound_map = $this->fieldHandler->getInboundMap($this->entityTypeId);
- $return = [];
- foreach ($values_per_entity as $entity_id => $values_per_graph) {
- $graph_uris = $this->getGraphHandler()->getEntityTypeGraphUris($this->getEntityTypeId());
- foreach ($graph_ids as $priority_graph_id) {
- foreach ($values_per_graph as $graph_uri => $entity_values) {
- // If the entity has been processed or the backend didn't returned
- // anything for this graph, jump to the next graph retrieved from the
- // SPARQL backend.
- if (isset($return[$entity_id]) || array_search($graph_uri, array_column($graph_uris, $priority_graph_id)) === FALSE) {
- continue;
- }
-
- $bundle = $this->getActiveBundle($entity_values);
- if (!$bundle) {
- continue;
- }
-
- // Check if the graph checked is in the request graphs. If there are
- // multiple graphs set, probably the default is requested with the
- // rest as fallback or it is a neutral call. If the default is
- // requested, it is going to be first in line so in any case, use the
- // first one.
- if (!$graph_id = $this->getGraphHandler()->getBundleGraphId($this->getEntityTypeId(), $bundle, $graph_uri)) {
- continue;
- }
+ $entity_array = $this->attachGraph($raw_entity, $entity_array);
- // Map bundle and entity id.
- $return[$entity_id][$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] = $bundle;
- $return[$entity_id][$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $entity_id;
- $return[$entity_id]['graph'][LanguageInterface::LANGCODE_DEFAULT] = $graph_id;
-
- $rdf_type = NULL;
- foreach ($entity_values as $predicate => $field) {
- $field_name = isset($inbound_map['fields'][$predicate][$bundle]['field_name']) ? $inbound_map['fields'][$predicate][$bundle]['field_name'] : NULL;
- if (empty($field_name)) {
- continue;
- }
+ foreach ($raw_entity as $predicate => $field) {
+ $entity_array = $this->attachField($predicate, $bundle, $field, $entity_array);
+ }
+ $translations = $this->getTranslations($entity_array);
+ return new $this->entityClass($entity_array, $this->entityTypeId, $bundle, $translations);
+ }
- $column = $inbound_map['fields'][$predicate][$bundle]['column'];
- foreach ($field as $lang => $items) {
- $langcode_key = ($lang === $default_language) ? LanguageInterface::LANGCODE_DEFAULT : $lang;
- foreach ($items as $delta => $item) {
- $item = $this->fieldHandler->getInboundValue($this->getEntityTypeId(), $field_name, $item, $langcode_key, $column, $bundle);
-
- if (!isset($return[$entity_id][$field_name][$langcode_key]) || !is_string($return[$entity_id][$field_name][$langcode_key])) {
- $return[$entity_id][$field_name][$langcode_key][$delta][$column] = $item;
- }
- }
- if (is_array($return[$entity_id][$field_name][$langcode_key])) {
- $this->applyFieldDefaults($inbound_map['fields'][$predicate][$bundle]['type'], $return[$entity_id][$field_name][$langcode_key]);
- }
- }
- }
+ /**
+ * @todo document!
+ */
+ protected function attachEntityId(array $entity_values, string $bundle, RawEntity $raw_entity) {
+ $entity_values[$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getSubject();
+ if ($this->fieldHandler->fieldIsMapped($this->entityTypeId, 'vid')) {
+ $rev_uris = $this->fieldHandler->getFieldPredicates($this->entityTypeId, 'vid', NULL, $bundle);
+ if ($rev_uris) {
+ $uri = array_pop($rev_uris);
+ // Loading a revision
+ if ($raw_entity->hasPredicate($uri)) {
+ $id = $raw_entity->getObjectDataByPredicate($uri)[LanguageInterface::LANGCODE_DEFAULT][0];
+ $entity_values[$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $id;
+ $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getSubject();
}
}
}
- return $return;
+ return $entity_values;
}
- /**
- * Deserializes a list of graph results to an array.
- *
- * The results array is an array of loaded entity values from different
- * graphs.
- * @code
- * $results = [
- * 'http://entity_id.uri' => [
- * 'http://field.mapping.uri' => [
- * 'x-default' => [
- * 0 => 'actual value'
- * ]
- * ]
- * ];
- * @code
- *
- * @param \EasyRdf\Sparql\Result|\EasyRdf\Result $results
- * A set of query results indexed per graph and entity id.
- *
- * @return array
- * The entity values indexed by the field mapping id.
- */
- protected function deserializeGraphResults(Result $results): array {
- $values_per_entity = [];
- foreach ($results as $result) {
- $entity_id = (string) $result->entity_id;
- $entity_graphs[$entity_id] = (string) $result->graph;
-
- $lang = LanguageInterface::LANGCODE_DEFAULT;
- if ($result->field_value instanceof Literal) {
- $lang_temp = $result->field_value->getLang();
- if ($lang_temp) {
- $lang = $lang_temp;
+ protected function getTranslations($entity_values) {
+ $langcode_key = $this->getEntityType()->getKey('langcode');
+ $translations = [];
+ if (!empty($entity_values[$langcode_key])) {
+ foreach ($entity_values[$langcode_key] as $langcode => $data) {
+ if (!empty(reset($data)['value'])) {
+ $translations[] = reset($data)['value'];
}
}
- $values_per_entity[$entity_id][(string) $result->graph][(string) $result->predicate][$lang][] = (string) $result->field_value;
}
-
- return $values_per_entity;
+ return $translations;
}
/**
* Derives the bundle from the rdf:type.
*
- * @param array $entity_values
+ * @param RawEntity $entity_values
* Entity in a raw formatted array.
*
* @return string
@@ -452,12 +419,12 @@ protected function deserializeGraphResults(Result $results): array {
* @throws \Exception
* Thrown when the bundle is not found.
*/
- protected function getActiveBundle(array $entity_values): ?string {
+ protected function getActiveBundle(RawEntity $entity_values): ?string {
$bundle_predicates = $this->bundlePredicate;
$bundles = [];
foreach ($bundle_predicates as $bundle_predicate) {
- if (isset($entity_values[$bundle_predicate])) {
- $bundle_data = $entity_values[$bundle_predicate];
+ if ($entity_values->hasPredicate($bundle_predicate)) {
+ $bundle_data = $entity_values->getObjectDataByPredicate($bundle_predicate);
$bundles += $this->fieldHandler->getInboundBundleValue($this->entityTypeId, $bundle_data[LanguageInterface::LANGCODE_DEFAULT][0]);
}
}
@@ -469,7 +436,7 @@ protected function getActiveBundle(array $entity_values): ?string {
// modules to handle this.
$this->moduleHandler->alter('rdf_load_bundle', $entity_values, $bundles);
if (count($bundles) > 1) {
- throw new \Exception('More than one bundles are defined for this uri.');
+ throw new \Exception('More than one bundle is defined for this uri.');
}
return reset($bundles);
}
@@ -623,9 +590,7 @@ public function loadUnchanged($id, array $graph_ids = NULL): ?ContentEntityInter
* {@inheritdoc}
*/
public function loadRevision($revision_id) {
- list($entity_id, $graph) = explode('||', $revision_id);
-
- return NULL;
+ return $this->load($revision_id);
}
/**
@@ -822,19 +787,27 @@ protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefiniti
* {@inheritdoc}
*/
protected function doSave($id, EntityInterface $entity) {
- $bundle = $entity->bundle();
+ if (!$entity instanceof RdfInterface) {
+ throw new \RuntimeException("Trying to persist an unsupported entity to RDF backend.");
+ }
+ $subject = $id;
// Generate an ID before saving, if none is available. If the ID generation
// occurs earlier in the process (like on EntityInterface::create()), the
// entity might be considered not new by modules that don't strictly use the
// EntityInterface::isNew() method.
if (empty($id)) {
- $id = $this->entityIdPluginManager->getPlugin($entity)->generate();
- $entity->{$this->idKey} = $id;
+ $subject = $id = $this->entityIdPluginManager->getPlugin($entity)->generate();
}
elseif ($entity->isNew() && $this->idExists($id)) {
throw new DuplicatedIdException("Attempting to create a new entity with the ID '$id' already taken.");
}
+ $entity->{$this->idKey} = $id;
+ if ($entity->isNewRevision()) {
+ $subject = $this->entityIdPluginManager->getPlugin($entity)->generate();
+ $entity->set('vid', $id);
+ }
+
// If the graph is not specified, fallback to the default one for the entity
// type.
if ($entity->get('graph')->isEmpty()) {
@@ -843,8 +816,50 @@ protected function doSave($id, EntityInterface $entity) {
$graph_id = $entity->get('graph')->target_id;
$graph_uri = $this->getGraphHandler()->getBundleGraphUri($entity->getEntityTypeId(), $entity->bundle(), $graph_id);
- $graph = self::getGraph($graph_uri);
- $lang_array = $this->toLangArray($entity);
+ $graph = $this->serialiseEntity(
+ self::createGraph($graph_uri),
+ $this->toLangArray($entity),
+ $entity->bundle(),
+ $subject
+ );
+
+ // Give implementations a chance to alter the graph right before is saved.
+ $this->alterGraph($graph, $entity);
+
+ if (!$entity->isNew()) {
+ $this->deleteBeforeInsert($id, $graph_uri);
+ }
+ try {
+ //
+ if ($entity->isDefaultRevision()) {
+ $rev_id = $entity->getRevisionId();
+ $entity->set('vid', NULL);
+ $graph = $this->serialiseEntity(
+ $graph,
+ $this->toLangArray($entity),
+ $entity->bundle(),
+ $rev_id
+ );
+ }
+ $this->insert($graph, $graph_uri);
+
+ return $entity->isNew() ? SAVED_NEW : SAVED_UPDATED;
+ }
+ catch (\Exception $e) {
+ return FALSE;
+ }
+ }
+
+ /**
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * @param array $lang_array
+ * @param string $bundle
+ * @param \EasyRdf\Graph $graph
+ * @param string $subject
+ *
+ * @throws \Drupal\rdf_entity\Exception\UnmappedFieldException
+ */
+ protected function serialiseEntity(Graph $graph, array $lang_array, string $bundle, string $subject): Graph {
foreach ($lang_array as $field_name => $langcode_data) {
foreach ($langcode_data as $langcode => $field_item) {
foreach ($field_item as $delta => $column_data) {
@@ -857,25 +872,12 @@ protected function doSave($id, EntityInterface $entity) {
$predicate = $this->fieldHandler->getFieldPredicates($this->getEntityTypeId(), $field_name, $column, $bundle);
$predicate = reset($predicate);
$value = $this->fieldHandler->getOutboundValue($this->getEntityTypeId(), $field_name, $value, $langcode, $column, $bundle);
- $graph->add((string) $id, $predicate, $value);
+ $graph->add($subject, $predicate, $value);
}
}
}
}
-
- // Give implementations a chance to alter the graph right before is saved.
- $this->alterGraph($graph, $entity);
-
- if (!$entity->isNew()) {
- $this->deleteBeforeInsert($id, $graph_uri);
- }
- try {
- $this->insert($graph, $graph_uri);
- return $entity->isNew() ? SAVED_NEW : SAVED_UPDATED;
- }
- catch (\Exception $e) {
- return FALSE;
- }
+ return $graph;
}
/**
@@ -1328,4 +1330,88 @@ protected function trackOriginalGraph(EntityInterface $entity): void {
$entity->rdfEntityOriginalGraph = $entity->get('graph')->target_id;
}
+ /**
+ * Hydrates a field and attaches it to the entity structure.
+ *
+ * @param $predicate
+ * The field predicate.
+ * @param string $bundle
+ * @param array $field
+ * @param array $entity
+ *
+ * @return array
+ */
+ protected function attachField($predicate, string $bundle, array $field, array $entity): array {
+ $inbound_map = $this->fieldHandler->getInboundMap($this->entityTypeId);
+ $field_name = isset($inbound_map['fields'][$predicate][$bundle]['field_name']) ? $inbound_map['fields'][$predicate][$bundle]['field_name'] : NULL;
+ if (empty($field_name)) {
+ return $entity;
+ }
+ assert(is_string($field_name), 'Field name must be string');
+ $default_language = $this->languageManager->getDefaultLanguage()->getId();
+ $column = $inbound_map['fields'][$predicate][$bundle]['column'];
+ foreach ($field as $lang => $items) {
+ $langcode_key = ($lang === $default_language) ? LanguageInterface::LANGCODE_DEFAULT : $lang;
+ foreach ($items as $delta => $item) {
+ $item = $this->fieldHandler->getInboundValue($this->getEntityTypeId(), $field_name, $item, $langcode_key, $column, $bundle);
+
+ if (!isset($entity[$field_name][$langcode_key]) || !is_string($entity[$field_name][$langcode_key])) {
+ $entity[$field_name][$langcode_key][$delta][$column] = $item;
+ }
+ }
+ if (is_array($entity[$field_name][$langcode_key])) {
+ $this->applyFieldDefaults($inbound_map['fields'][$predicate][$bundle]['type'], $entity[$field_name][$langcode_key]);
+ }
+ }
+ return $entity;
+ }
+
+ /**
+ * @param \Drupal\rdf_entity\RawEntityRepository $filtered_result_set
+ *
+ * @return array
+ * @throws \Exception
+ */
+ protected function hydrateRepository(RawEntityRepository $filtered_result_set): array {
+ $entity_list = [];
+ foreach ($filtered_result_set as $raw_entity) {
+ if ($entity = $this->hydrateEntity($raw_entity)) {
+ $entity_list[$entity->id()] = $entity;
+ }
+ }
+ return $entity_list;
+ }
+
+ /**
+ * @param \Drupal\rdf_entity\RawEntity $raw_entity
+ * @param $entity_array
+ *
+ * @return array
+ */
+ protected function attachGraph(RawEntity $raw_entity, $entity_array): array {
+ $def = $this->getGraphHandler()
+ ->getEntityTypeGraphUris($this->entityTypeId);
+ foreach ($def as $bundle => $bundle_data) {
+ foreach ($bundle_data as $graph_id => $graph_uri) {
+ if ($graph_uri === $raw_entity->getGraphUri()) {
+ $entity_array['graph'][LanguageInterface::LANGCODE_DEFAULT] = $graph_id;
+ }
+ }
+ }
+ return $entity_array;
+ }
+
+ /**
+ * @param string|null $bundle
+ * @param array $entity_array
+ *
+ * @return array
+ */
+ protected function attachBundle(?string $bundle, array $entity_array): array {
+ if ($this->bundleKey) {
+ $entity_array[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] = $bundle;
+ }
+ return $entity_array;
+ }
+
}
diff --git a/src/Form/RdfRevisionDeleteForm.php b/src/Form/RdfRevisionDeleteForm.php
new file mode 100644
index 00000000..96c9cb3e
--- /dev/null
+++ b/src/Form/RdfRevisionDeleteForm.php
@@ -0,0 +1,55 @@
+sparqlStorage = $sparql_storage;
+ $this->dateFormatter = $date_formatter;
+ $this->time = $time;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager')->getStorage('rdf_entity'),
+ $container->get('date.formatter'),
+ $container->get('datetime.time')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'rdf_entity_revision_revert_confirm';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+
+ $date_formatted = $this->dateFormatter->format($this->getRevisionTime());
+ return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $date_formatted]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.rdf_entity.version_history', ['rdf_entity' => $this->revision->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return t('Revert');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $rdf_revision = NULL) {
+ $this->revision = $rdf_revision;
+ $form = parent::buildForm($form, $form_state);
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // The revision timestamp will be updated when the revision is saved. Keep
+ // the original one for the confirmation message.
+ $original_revision_timestamp = $this->getRevisionTime();
+
+ $this->revision = $this->prepareRevertedRevision($this->revision, $form_state);
+ $this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]);
+ $this->revision->setRevisionUserId($this->currentUser()->id());
+ $this->revision->setRevisionCreationTime($this->time->getRequestTime());
+ $this->revision->setChangedTime($this->time->getRequestTime());
+ $this->revision->save();
+
+ $this->logger('content')->notice('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
+ $this->messenger()
+ ->addStatus($this->t('@type %title has been reverted to the revision from %revision-date.', [
+ '@type' => $this->revision->bundle(),
+ '%title' => $this->revision->label(),
+ '%revision-date' => $this->dateFormatter->format($original_revision_timestamp),
+ ]));
+ $form_state->setRedirect(
+ 'entity.rdf_entity.version_history',
+ ['rdf_entity' => $this->revision->id()]
+ );
+ }
+
+ protected function getRevisionTime() {
+ $date = $this->revision->getRevisionCreationTime();
+ if (!$date) {
+ $date = $this->revision->getCreatedTime();
+ }
+ return $date;
+ }
+
+ /**
+ * Prepares a revision to be reverted.
+ *
+ * @param \Drupal\node\NodeInterface $revision
+ * The revision to be reverted.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return \Drupal\node\NodeInterface
+ * The prepared revision ready to be stored.
+ */
+ protected function prepareRevertedRevision(RdfInterface $revision, FormStateInterface $form_state) {
+ $revision->setNewRevision();
+ $revision->isDefaultRevision(TRUE);
+
+ return $revision;
+ }
+
+}
diff --git a/src/Form/RdfRevisionRevertTranslationForm.php b/src/Form/RdfRevisionRevertTranslationForm.php
new file mode 100644
index 00000000..e0b4be2d
--- /dev/null
+++ b/src/Form/RdfRevisionRevertTranslationForm.php
@@ -0,0 +1,42 @@
+entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * Created a filtered entity repository.
+ *
+ * The new will contain one entry for each subject. If an entity is
+ * present in multiple graphs, the entity from the graph with the highest
+ * priority will be selected.
+ *
+ * @param \Drupal\rdf_entity\RawEntityRepository $entity_repository
+ * @param $graph_priorities
+ * @param $entity_type_id
+ *
+ * @return \Drupal\rdf_entity\RawEntityRepository
+ * @throws \Exception
+ */
+ public function filter(RawEntityRepository $entity_repository, $graph_priorities, $entity_type_id) : RawEntityRepository {
+ $filtered_set = new RawEntityRepository();
+
+ foreach ($graph_priorities as $graph_priority) {
+ $uris = $this->graphUrisFromPriority($graph_priority, $entity_type_id);
+ $priority_result_set = $entity_repository->newRepoFromGraphUris($uris);
+ $filtered_set = $filtered_set->merge($priority_result_set);
+ }
+ return $filtered_set;
+ }
+
+
+ protected function graphUrisFromPriority(string $priority, $entity_type_id) {
+ $storage = $this->entityTypeManager->getStorage($entity_type_id);
+ if (!$storage instanceof RdfEntitySparqlStorageInterface) {
+ throw new \Exception('Storage must implement RDF Storage interface.');
+ }
+ $graph_uris = $storage->getGraphHandler()->getEntityTypeGraphUris($entity_type_id);
+ return array_column($graph_uris, $priority);
+ }
+}
\ No newline at end of file
diff --git a/src/RawEntity.php b/src/RawEntity.php
new file mode 100644
index 00000000..9f97bec9
--- /dev/null
+++ b/src/RawEntity.php
@@ -0,0 +1,164 @@
+graph = $graph;
+ $this->subject = $subject;
+ }
+
+ /**
+ * Gets the subject of the entity.
+ *
+ * @return string
+ * The subject URI.
+ */
+ public function getSubject() {
+ return $this->subject;
+ }
+
+ /**
+ * Gets the originating graph of the entity.
+ * @return string
+ * The graph URI.
+ */
+ public function getGraphUri() {
+ return $this->graph;
+ }
+
+ /**
+ * Adds a predicate-object pair to the entity.
+ *
+ * @todo Add support for blank nodes.
+ *
+ * @param string $predicate
+ * The URI of the predicate.
+ * @param $object
+ * A literal or resource.
+ */
+ public function add(string $predicate, $object) {
+ $language = $this->getLanguage($object);
+ $this->objects[$predicate][$language][] = (string) $object;
+ }
+
+ /**
+ * Checks if an object with a given predicate exists in the set.
+ *
+ * @param $predicate
+ * The URI of the predicate.
+ *
+ * @return bool
+ * True if an object with said predicate exits in the set.
+ */
+ public function hasPredicate($predicate) {
+ return isset($this->objects[$predicate]);
+ }
+
+ /**
+ * Returns the language/object structure for given predicate.
+ *
+ * @param $predicate
+ * The URI of the predicate.
+ *
+ * @return mixed
+ */
+ public function getObjectDataByPredicate($predicate) {
+ return $this->objects[$predicate];
+ }
+
+ /**
+ * Determine the language of an object.
+ *
+ * @param \EasyRdf\Resource|\EasyRdf\Literal $object
+ * The object for which the language will be determined.
+ *
+ * @return string
+ * The language code of the object.
+ */
+ protected function getLanguage($object): string {
+ $language = LanguageInterface::LANGCODE_DEFAULT;
+ if ($object instanceof Literal) {
+ $object_language = $object->getLang();
+ if ($object_language) {
+ $language = $object_language;
+ }
+ }
+ return $language;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind() {
+ reset($this->objects);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current() : array {
+ return current($this->objects);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key() :string {
+ return key($this->objects);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next() {
+ next($this->objects);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid() {
+ return key($this->objects) !== NULL;
+ }
+
+}
diff --git a/src/RawEntityRepository.php b/src/RawEntityRepository.php
new file mode 100644
index 00000000..12ef89b0
--- /dev/null
+++ b/src/RawEntityRepository.php
@@ -0,0 +1,202 @@
+graph;
+ $subject = (string) $result->entity_subject;
+ $predicate = (string) $result->predicate;
+ $object = $result->field_value;
+
+ $repo->addResult($graph, $subject, $predicate, $object);
+ }
+ return $repo;
+ }
+
+ /**
+ * Adds a value to the repo. Creates new EntityValue object if needed.
+ *
+ * @param string $graph
+ * The graph URI.
+ * @param string $subject
+ * The subject URI.
+ * @param string $predicate
+ * The predicate URI.
+ * @param $object
+ * The object.
+ */
+ protected function addResult(string $graph, string $subject, string $predicate, $object) {
+ $entity_values = $this->loadCreateRawEntity($graph, $subject);
+ $entity_values->add($predicate, $object);
+ }
+
+ /**
+ * Loads a raw entity from the repo if it exists, or creates one if not.
+ *
+ * @param $graph
+ * The graph URI.
+ * @param $subject
+ * The subject URI.
+ *
+ * @return \Drupal\rdf_entity\RawEntity
+ */
+ protected function loadCreateRawEntity($graph, $subject) : RawEntity {
+ if (isset($this->repoBySubject[$subject][$graph])) {
+ return $this->repoBySubject[$subject][$graph];
+ }
+ return $this->createRawEntity($graph, $subject);
+ }
+
+ /**
+ * Create a new raw entity, and add it to the repo.
+ *
+ * @param $graph
+ * The graph URI.
+ * @param $subject
+ * The subject URI.
+ *
+ * @return \Drupal\rdf_entity\RawEntity
+ * The entity values object.
+ */
+ protected function createRawEntity($graph, $subject) : RawEntity {
+ $sparql_result = new RawEntity($graph, $subject);
+
+ $this->trackEntity($sparql_result);
+ return $sparql_result;
+ }
+
+ /**
+ * Associates a raw entity with the repo.
+ *
+ * @param \Drupal\rdf_entity\RawEntity $raw_entity
+ * The raw entity to keep track of.
+ */
+ public function trackEntity(RawEntity $raw_entity) {
+ // Add the same object the lookup tables.
+ $this->repoBySubject[$raw_entity->getSubject()][$raw_entity->getGraphUri()] = $raw_entity;
+ $this->repoFlat[] = $raw_entity;
+ }
+
+ /**
+ * Create new immutable entity repo, filtered by graph.
+ *
+ * @param $uris The graph uri to filter on.
+ *
+ * @return \Drupal\rdf_entity\RawEntityRepository
+ */
+ public function newRepoFromGraphUris($uris) {
+ $filtered_entity_repo = new RawEntityRepository();
+ foreach ($this as $raw_entity) {
+ if (in_array($raw_entity->getGraphUri(), $uris)) {
+ $filtered_entity_repo->trackEntity($raw_entity);
+ }
+ }
+ return $filtered_entity_repo;
+ }
+
+ /**
+ * Merges two entity repos into a new one.
+ *
+ * Only results with a subject that is not present in the set get merged in.
+ *
+ * @param \Drupal\rdf_entity\RawEntityRepository $repo_to_merge
+ *
+ * @return \Drupal\rdf_entity\RawEntityRepository
+ */
+ public function merge(RawEntityRepository $repo_to_merge) {
+ $merged_repo = clone $this;
+ foreach ($repo_to_merge as $entity_to_merge) {
+ if (!$merged_repo->hasSubject($entity_to_merge->getSubject())) {
+ $merged_repo->trackEntity($entity_to_merge);
+ }
+ }
+ return $merged_repo;
+ }
+
+ /**
+ * A raw entity with the given subject is present in the repo.
+ *
+ * @param $subject
+ * The URI of the subject.
+ *
+ * @return bool
+ * True if the raw entity was found.
+ */
+ public function hasSubject($subject) {
+ return isset($this->repoBySubject[$subject]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind() {
+ $this->position = 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current() : RawEntity{
+ return $this->repoFlat[$this->position];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key() :string {
+ return $this->position;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next() {
+ ++$this->position;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid() {
+ return isset($this->repoFlat[$this->position]);
+ }
+
+}
\ No newline at end of file
diff --git a/src/RdfInterface.php b/src/RdfInterface.php
index 86bee8ce..7b7d7fb4 100755
--- a/src/RdfInterface.php
+++ b/src/RdfInterface.php
@@ -5,6 +5,7 @@
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\user\EntityOwnerInterface;
/**
@@ -14,7 +15,7 @@
*
* @ingroup rdf_entity
*/
-interface RdfInterface extends ContentEntityInterface, EntityPublishedInterface, EntityOwnerInterface, EntityChangedInterface {
+interface RdfInterface extends ContentEntityInterface, EntityPublishedInterface, EntityOwnerInterface, EntityChangedInterface, RevisionLogInterface {
/**
* Gets the name of the rdf entity.
diff --git a/src/RouteProcessor/RouteProcessorRdf.php b/src/RouteProcessor/RouteProcessorRdf.php
index 7101344c..0d9b1696 100644
--- a/src/RouteProcessor/RouteProcessorRdf.php
+++ b/src/RouteProcessor/RouteProcessorRdf.php
@@ -48,7 +48,8 @@ public function __construct(RouteMatchInterface $route_match) {
*/
public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
if ($route->hasOption('parameters')) {
- foreach ($route->getOption('parameters') as $type => $parameter) {
+ $option = $route->getOption('parameters');
+ foreach ($option as $type => $parameter) {
// If the rdf_entity converter exists in the parameter,
// then the parameter is of type rdf_entity and needs to be normalized.
if (isset($parameter['converter']) && $parameter['converter'] == 'paramconverter.rdf_entity' && SparqlArg::isValidResource($parameters[$type])) {