diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..91604ba --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "square-bit/views_oai_pmh", + "type": "drupal-module", + "description": "Views plugins to publish data through the OAI-PMH protocol.", + "keywords": ["Drupal"], + "license": "GPL-2.0-or-later", + "minimum-stability": "dev", + "require": { + "symfony/psr-http-message-bridge": "^2.1", + "picturae/oai-pmh": "^0.5.20", + "nyholm/psr7": "^1.4" + } +} diff --git a/src/Annotation/MetadataPrefix.php b/src/Annotation/MetadataPrefix.php new file mode 100644 index 0000000..12e4a7d --- /dev/null +++ b/src/Annotation/MetadataPrefix.php @@ -0,0 +1,36 @@ + $this->t('- None -'), + 'titles>title' => 'titles > title', + 'publisher' => 'publisher', + 'subjects>subject' => 'subjects>subject', + 'subjects>subject@subjectScheme' => 'subjects>subject@subjectScheme', + 'dates>date' => 'dates>date', + 'dates>date@dateType' => 'dates>date@dateType', + 'descriptions>description' => 'descriptions>description', + 'descriptions>description@descriptionType' => 'descriptions>description@descriptionType', + 'publicationYear' => 'publicationYear', + 'identifier' => 'identifier', + 'identifier@identifierType' => 'identifier@identifierType', + 'creators>creator>creatorName' => 'creators>creator>creatorName', + 'creators>creator>nameIdentifier' => 'creators>creator>nameIdentifier', + 'creators>creator>nameIdentifier@nameIdentifierScheme' => 'creators>creator>nameIdentifier@nameIdentifierScheme', + 'creators>creator>nameIdentifier@schemeURI' => 'creators>creator>nameIdentifier@schemeURI', + 'contributors>contributor>contributorName' => 'contributors>contributor>contributorName', + 'contributors>contributor>contributorName@contributorType' => 'contributors>contributor>contributorName@contributorType', + 'contributors>contributor>nameIdentifier' => 'contributors>contributor>nameIdentifier', + 'contributors>contributor>nameIdentifier@nameIdentifierScheme' => 'contributors>contributor>nameIdentifier@nameIdentifierScheme', + 'contributors>contributor>nameIdentifier@schemeURI' => 'contributors>contributor>nameIdentifier@schemeURI', + 'resourceType' => 'resourceType', + 'resourceType@resourceTypeGeneral' => 'resourceType@resourceTypeGeneral', + 'rightsList>rights' => 'rightsList>rights', + 'rightsList>rights@rightsURI' => 'rightsList>rights@rightsURI', + ]; + } + +} diff --git a/src/Plugin/MetadataPrefix/DublinCore.php b/src/Plugin/MetadataPrefix/DublinCore.php new file mode 100644 index 0000000..68b63e7 --- /dev/null +++ b/src/Plugin/MetadataPrefix/DublinCore.php @@ -0,0 +1,76 @@ + 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd', + '@xmlns:oai_dc' => 'http://www.openarchives.org/OAI/2.0/oai_dc/', + '@xmlns:dc' => 'http://purl.org/dc/elements/1.1/', + ]; + } + + /** + * + */ + public function getElements(): array { + return [ + 'none' => t('- None -'), + 'dc:title' => 'dc:title', + 'dc:creator' => 'dc:creator', + 'dc:subject' => 'dc:subject', + 'dc:description' => 'dc:description', + 'dc:publisher' => 'dc:publisher', + 'dc:contributor' => 'dc:contributor', + 'dc:date' => 'dc:date', + 'dc:type' => 'dc:type', + 'dc:format' => 'dc:format', + 'dc:identifier' => 'dc:identifier', + 'dc:source' => 'dc:source', + 'dc:language' => 'dc:language', + 'dc:relation' => 'dc:relation', + 'dc:coverage' => 'dc:coverage', + 'dc:rights' => 'dc:rights', + ]; + } + +} diff --git a/src/Plugin/MetadataPrefix/Jats.php b/src/Plugin/MetadataPrefix/Jats.php new file mode 100644 index 0000000..a415952 --- /dev/null +++ b/src/Plugin/MetadataPrefix/Jats.php @@ -0,0 +1,160 @@ + $this->t('- None -'), + + '@xmlns' => $this->t('(Root attribute) xmlns'), + '@xmlns:xlink' => $this->t('(Root attribute) xmlns:xlink'), + '@dtd-version' => $this->t('(Root attribute) dtd-version'), + '@specific-use' => $this->t('(Root attribute) specific-use'), + '@article-type' => $this->t('(Root attribute) article-type'), + '@xml:lang' => $this->t('(Root attribute) xml:lang'), + + 'front>journal-meta>journal-id' => 'front > journal-meta > journal-id', + 'front>journal-meta>journal-id@journal-id-type' => 'front > journal-meta > journal-id@journal-id-type', + + 'front>journal-meta>journal-title-group>journal-title' => 'front > journal-meta > journal-title-group > journal-title', + 'front>journal-meta>journal-title-group>journal-subtitle' => 'front > journal-meta > journal-title-group > journal-subtitle', + 'front>journal-meta>journal-title-group>abbrev-journal-title' => 'front > journal-meta > journal-title-group > abbrev-journal-title', + + 'front>journal-meta>contrib-group@content-type' => 'front > journal-meta > contrib-group@content-type', + 'front>journal-meta>contrib-group>contrib@contrib-type' => 'front > journal-meta > contrib@contrib-type', + 'front>journal-meta>contrib-group>contrib>name>surname' => 'front > journal-meta > contrib > name > surname', + 'front>journal-meta>contrib-group>contrib>name>given-names' => 'front > journal-meta > contrib > name > given-names', + 'front>journal-meta>contrib-group>contrib>role' => 'front > journal-meta > contrib > role', + + 'front>journal-meta>issn' => 'front > journal-meta > issn', + 'front>journal-meta>issn@pub-type' => 'front > journal-meta > issn@pub-type', + + 'front>journal-meta>publisher>publisher-name' => 'front > journal-meta > publisher > publisher-name', + + 'front>article-meta>article-id' => 'front > article-meta > article-id', + 'front>article-meta>article-id@pub-id-type' => 'front > article-meta > article-id@pub-id-type', + + 'front>article-meta>article-categories>subj-group@subj-group-type' => 'front > article-meta > article-categories > subj-group@subj-group-type', + 'front>article-meta>article-categories>subj-group>subject' => 'front > article-meta > article-categories > subj-group > subject', + 'front>article-meta>article-categories>subj-group>subj-group@subj-group-type' => 'front > article-meta > article-categories > subj-group > subj-group@subj-group-type', + 'front>article-meta>article-categories>subj-group>subj-group>subject' => 'front > article-meta > article-categories > subj-group > subj-group > subject', + + 'front>article-meta>title-group>article-title' => 'front > article-meta > title-group > article-title', + 'front>article-meta>title-group>article-title@xml:lang' => 'front > article-meta > title-group > article-title@xml:lang', + 'front>article-meta>title-group>subtitle' => 'front > article-meta > title-group > subtitle', + 'front>article-meta>title-group>subtitle@xml:lang' => 'front > article-meta > title-group > subtitle@xml:lang', + + 'front>article-meta>volume' => 'front > article-meta > volume', + + 'front>article-meta>contrib-group@content-type' => 'front > article-meta > contrib-group@content-type', + 'front>article-meta>contrib-group>contrib@contrib-type' => 'front > article-meta > contrib-group > contrib@contrib-type', + 'front>article-meta>contrib-group>contrib@id' => 'front > article-meta > contrib-group > contrib@id', + 'front>article-meta>contrib-group>contrib>name>surname' => 'front > article-meta > contrib-group > contrib > name > surname', + 'front>article-meta>contrib-group>contrib>name>given-names' => 'front > article-meta > contrib-group > contrib > name > given-names', + + 'front>article-meta>contrib-group>contrib>xref@rid' => 'front > article-meta > contrib-group > contrib > xref@rid', + 'front>article-meta>contrib-group>contrib>xref@ref-type' => 'front > article-meta > contrib-group > contrib > xref@ref-type', + + 'front>article-meta>contrib-group>contrib>contrib-id' => 'front > article-meta > contrib-group > contrib > contrib-id', + 'front>article-meta>contrib-group>contrib>contrib-id@contrib-id-type' => 'front > article-meta > contrib-group > contrib > contrib-id@contrib-id-type', + + 'front>article-meta>aff' => 'front > article-meta > aff', + 'front>article-meta>institution' => 'front > article-meta > aff > institution', + + 'front>article-meta>contrib-group>contrib>bio' => 'front > article-meta > contrib-group > contrib > bio', + 'front>article-meta>contrib-group>contrib>bio@xml:lang' => 'front > article-meta > contrib-group > contrib > bio@xml:lang', + + 'front>article-meta>pub-date>season' => 'front > article-meta > pub-date > season', + 'front>article-meta>pub-date@publication-format' => 'front > article-meta > pub-date@publication-format', + 'front>article-meta>pub-date@date-type' => 'front > article-meta > pub-date@date-type', + 'front>article-meta>pub-date>day' => 'front > article-meta > pub-date > day', + 'front>article-meta>pub-date>month' => 'front > article-meta > pub-date > month', + 'front>article-meta>pub-date>year' => 'front > article-meta > pub-date > year', + + 'front>article-meta>issue' => 'front > article-meta > issue', + 'front>article-meta>issue@seq' => 'front > article-meta > issue@seq', + + 'front>article-meta>issue-id' => 'front > article-meta > issue-id', + 'front>article-meta>issue-id@pub-id-type' => 'front > article-meta > issue-id@pub-id-type', + 'front>article-meta>issue-title' => 'front > article-meta > issue-title', + + 'front>article-meta>fpage' => 'front > article-meta > fpage', + 'front>article-meta>lpage' => 'front > article-meta > lpage', + + 'front>article-meta>permissions>copyright-statement' => 'front > article-meta > permissions > copyright-statement', + 'front>article-meta>permissions>copyright-year' => 'front > article-meta > permissions > copyright-year', + 'front>article-meta>permissions>copyright-holder' => 'front > article-meta > permissions > copyright-holder', + 'front>article-meta>permissions>licence@xlink:href' => 'front > article-meta > permissions > licence@xlink:href', + 'front>article-meta>permissions>licence>licence-p>graphic@xlink:href' => 'front > article-meta > permissions > licence > licence-p > graphic@xlink:href', + + 'front>article-meta>abstract' => 'front > article-meta > abstract', + 'front>article-meta>abstract@xml:lang' => 'front > article-meta > abstract@xml:lang', + 'front>article-meta>abstract>p' => 'front > article-meta > abstract > p', + 'front>article-meta>trans-abstract@xml:lang' => 'front > article-meta > trans-abstract@xml:lang', + 'front>article-meta>trans-abstract>p' => 'front > article-meta > trans-abstract > p', + + 'front>article-meta>related-object' => 'front > article-meta > related-object', + 'front>article-meta>related-object@content-type' => 'front > article-meta > related-object@content-type', + 'front>article-meta>related-object@document-type' => 'front > article-meta > related-object@document-type', + + 'body' => 'body', + 'body>p' => 'body > p', + + 'back>ref-list>ref@id' => 'back > ref-list > ref@id', + 'back>ref-list>ref>element-citation>styled-content' => 'back > ref-list > ref > element-citation > styled-content', + 'back>ref-list>ref>element-citation>styled-content@specific-use' => 'back > ref-list > ref > element-citation > styled-content@specific-use', + 'back>ref-list>ref>element-citation>pub-id' => 'back > ref-list > ref > element-citation > pub-id', + 'back>ref-list>ref>element-citation>pub-id@pub-id-type' => 'back > ref-list > ref > element-citation > pub-id@pub-id-type', + ]; + } + +} diff --git a/src/Plugin/MetadataPrefixBase.php b/src/Plugin/MetadataPrefixBase.php new file mode 100644 index 0000000..eea9f73 --- /dev/null +++ b/src/Plugin/MetadataPrefixBase.php @@ -0,0 +1,14 @@ +alterInfo('views_oai_pmh_views_oai_pmh_prefix_info'); + $this->setCacheBackend($cache_backend, 'views_oai_pmh_views_oai_pmh_prefix_plugins'); + } + +} diff --git a/src/Plugin/views/display/OAIPMH.php b/src/Plugin/views/display/OAIPMH.php new file mode 100644 index 0000000..aaff986 --- /dev/null +++ b/src/Plugin/views/display/OAIPMH.php @@ -0,0 +1,245 @@ +renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('router.route_provider'), + $container->get('state'), + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return 'oai_pmh'; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + // Set the default style plugin to 'json'. + $options['style']['contains']['type']['default'] = 'views_oai_pmh_record'; + $options['defaults']['default']['style'] = FALSE; + $options['defaults']['default']['row'] = FALSE; + + // Remove css/exposed form settings, as they are not used for the data display. + unset($options['exposed_form']); + unset($options['exposed_block']); + unset($options['css_class']); + + return $options; + } + + /** + * + */ + public function optionsSummary(&$categories, &$options) { + parent::optionsSummary($categories, $options); + $categories['page']['title'] = $this->t('OAI-PMH settings'); + $categories['title']['title'] = $this->t('Repository name'); + $options['title']['title'] = $this->t('Repository name'); + } + + /** + * + */ + public static function buildResponse($view_id, $display_id, array $args = []) { + $build = static::buildBasicRenderable($view_id, $display_id, $args); + + // Setup an empty response so headers can be added as needed during views + // rendering and processing. + $response = new CacheableResponse('', 200); + $build['#response'] = $response; + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + $output = (string) $renderer->renderRoot($build); + + $response->setContent($output); + $cache_metadata = CacheableMetadata::createFromRenderArray($build); + $response->addCacheableDependency($cache_metadata); + $response->headers->set('Content-Type', 'application/xml;charset=UTF-8'); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = []; + + $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () { + return $this->view->style_plugin->render(); + }); + + $this->view->element['#cache_properties'][] = '#content_type'; + + // Encode and wrap the output in a pre tag if this is for a live preview. + if (!empty($this->view->live_preview)) { + $build['#prefix'] = '
';
+      $build['#plain_text'] = $build['#markup'];
+      $build['#suffix'] = '
'; + unset($build['#markup']); + } + else { + $build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']); + } + + parent::applyDisplayCacheabilityMetadata($build); + + return $build; + } + + /** + * {@inheritdoc} + */ + public function execute() { + $this->view->setCurrentPage($this->pageByResumptionToken()); + + parent::execute(); + + return $this->view->render(); + } + + /** + * {@inheritdoc} + * + * The DisplayPluginBase preview method assumes we will be returning a render + * array. The data plugin will already return the serialized string. + */ + public function preview() { + return $this->view->render(); + } + + /** + * + */ + public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) { + parent::initDisplay($view, $display, $options); + + if (!$prefix = $this->getMetadataPrefixByToken()) { + $prefix = 'oai_dc'; + } + + $this->metadataPrefix = $this->view + ->getRequest() + ->query + ->get('metadataPrefix', $prefix); + } + + /** + * + */ + public function getCurrentMetadataPrefix() { + return $this->metadataPrefix; + } + + private function pageByResumptionToken() { + $token = $this->view->getRequest() + ->query + ->get('resumptionToken', NULL); + + if ($token) { + /** @var Repository $repository */ + $repository = \Drupal::service('views_oai_pmh.repository'); + $paginator = $repository->decodeResumptionToken($token); + + return $paginator['offset']; + } + + return 0; + } + + private function getMetadataPrefixByToken() { + $token = $this->view->getRequest() + ->query + ->get('resumptionToken', NULL); + + if ($token) { + /** @var Repository $repository */ + $repository = \Drupal::service('views_oai_pmh.repository'); + $paginator = $repository->decodeResumptionToken($token); + + return $paginator['metadataPrefix']; + } + + return NULL; + } +} diff --git a/src/Plugin/views/style/Record.php b/src/Plugin/views/style/Record.php new file mode 100644 index 0000000..abb40cf --- /dev/null +++ b/src/Plugin/views/style/Record.php @@ -0,0 +1,515 @@ +get('views_oai_pmh.format_row_xml'), + $container->get('plugin.manager.views_oai_pmh_prefix'), + $container->get('serializer'), + $container->get('views_oai_pmh.repository'), + $container->get('views_oai_pmh.provider') + ); + } + + /** + * Record constructor. + * + * @param array $configuration + * @param $plugin_id + * @param $plugin_definition + * @param \Drupal\views_oai_pmh\Service\FormatRowToXml $rowToXml + * @param \Drupal\views_oai_pmh\Plugin\MetadataPrefixManager $prefixManager + * @param \Symfony\Component\Serializer\Serializer $serializer + * @param \Drupal\views_oai_pmh\Service\Repository $repository + * @param \Drupal\views_oai_pmh\Service\Provider $provider + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, FormatRowToXml $rowToXml, MetadataPrefixManager $prefixManager, Serializer $serializer, Repository $repository, Provider $provider) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->rowToXml = $rowToXml; + $this->prefixManager = $prefixManager; + $this->serializer = $serializer; + $this->repository = $repository; + $this->provider = $provider; + + foreach ($prefixManager->getDefinitions() as $id => $plugin) { + $this->metadataPrefix[$id] = $plugin; + } + } + + /** + * @param $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $handlers = $this->displayHandler->getHandlers('field'); + if (empty($handlers)) { + $form['error_markup'] = [ + '#markup' => '
' . $this->t('You need at least one field before you can configure your table settings') . '
', + ]; + return; + } + + $formats = []; + foreach ($this->metadataPrefix as $prefix_id => $prefix) { + $formats[$prefix_id] = $this->t($prefix['label']); + } + + $form['enabled_formats'] = [ + '#type' => 'checkboxes', + '#title' => t('OAI-PMH metadata formats'), + '#description' => t('Select the metadata format(s) that you wish to publish. Note that the Dublin Core format must remain enabled as it is required by the OAI-PMH standard.'), + '#default_value' => $this->options['enabled_formats'], + '#options' => $formats, + ]; + + $form['metadata_prefix'] = [ + '#type' => 'fieldset', + '#title' => t('Metadata prefixes'), + ]; + + $field_labels = $this->displayHandler->getFieldLabels(); + foreach ($this->metadataPrefix as $prefix_id => $prefix) { + $form['metadata_prefix'][$prefix_id] = [ + '#type' => 'textfield', + '#title' => $prefix['label'], + '#default_value' => $this->options['metadata_prefix'][$prefix_id] ? $this->options['metadata_prefix'][$prefix_id] : $prefix['prefix'], + '#required' => TRUE, + '#size' => 16, + '#maxlength' => 32, + ]; + $form['field_mappings'][$prefix_id] = [ + '#type' => 'fieldset', + '#title' => t('Field mappings for @format', ['@format' => $prefix['label']]), +// '#theme' => 'views_oai_pmh_field_mappings_form', + '#states' => [ + 'visible' => [ + ':input[name="style_options[enabled_formats][' . $prefix_id . ']"]' => ['checked' => TRUE], + ], + ], + ]; + + $prefixPlugin = $this->getInstancePlugin($prefix_id); + foreach ($this->displayHandler->getOption('fields') as $field_name => $field) { + $form['field_mappings'][$prefix_id][$field_name] = [ + '#type' => 'select', + '#options' => $prefixPlugin->getElements(), + '#default_value' => !empty($this->options['field_mappings'][$prefix_id][$field_name]) ? $this->options['field_mappings'][$prefix_id][$field_name] : '', + '#title' => $field_labels[$field_name], + ]; + } + } + + } + + /** + * {@inheritdoc} + */ + public function render() { + $rows = $this->getResultRows(); + + /** @var \Drupal\views_oai_pmh\Plugin\MetadataPrefixInterface $currentPrefixPlugin */ + $currentPrefixPlugin = $this->prefixManager->createInstance( + $this->displayHandler->getCurrentMetadataPrefix() + ); + + $records = []; + foreach ($rows as $row_id => $row) { + $this->rowToXml->resetTagsPrefixedWith0(); + + // Getting all the elements. + $elements = $this->rowToXml->transform($row); + + $element_or_null = array_shift($elements); + // Insure we're adding (union) arrays, not null. + $element = $element_or_null ? $element_or_null : array(); + // Getting only the root elements, we need to remove the root elements + // from the list with all the elements to prepare the correct structure. + $root_elements = $element + $currentPrefixPlugin->getRootNodeAttributes(); + + // Getting the elements in the level the need to be. + $data = $root_elements + $elements; + + // path id for datacite, dcc or dc + $path_id = (!empty($data['identifier']))? $data['identifier']['#'] : $data['dc:identifier']['#']; + + $xmlDoc = new \DOMDocument(); + $xmlDoc->loadXML($this->serializer->encode($data, 'xml', [ + 'xml_root_node_name' => $currentPrefixPlugin->getRootNodeName(), + ])); + + // Removing empty elements. + $xpath = new \DOMXPath($xmlDoc); + foreach( $xpath->query('//*[not(node())]') as $node ) { + $node->parentNode->removeChild($node); + } + + $header = new Header($this->getIdentifier($path_id), new \DateTime()); + $records[$row_id] = new OAIRecord($header, $xmlDoc); + } + + $formats = []; + foreach ($this->options['enabled_formats'] as $format) { + if ($format) { + $plugin = $this->getInstancePlugin($format); + $formats[] = new MetadataFormatType( + $format, + $plugin->getSchema(), + $plugin->getNamespace() + ); + } + } + + $this->repository->setRecords($records); + + if ($pager = $this->view->pager->hasMoreRecords()) { + $this->repository->setOffset($this->view->getCurrentPage() + 1); + $this->repository->setTotalRecords($this->view->total_rows); + } + + $this->repository->setMetadataFormats($formats); + + return $this->provider; + } + + /** + * The identifier for record header + * + * @param $id + * + * @return string + * format: oai:domain/nid + */ + protected function getIdentifier($id) { + $path = ""; + if(strpos($id,'https://') !== false) { + $path = str_replace("https://", "oai:", $id); + } else if(strpos($id,'http://') !== false) { + $path = str_replace("http://", "oai:", $id); + } + return $path; + } + + /** + * Get result that view expose as cartesian product removing duplicates tuples + * + * @return array + */ + protected function getResultRows(): array { + $rows = []; + foreach ($this->view->result as $row_id => $row) { + $this->view->row_index = $row_id; + $item = $this->populateRow($row_id, $row); + $id = $row->_entity->id(); + + if (key_exists($id, $rows)) { + $rows[$id] = array_merge_recursive($rows[$id], $item); + } + else { + $rows[$id] = $item; + } + } + //$rows = $this->removeDuplicates($rows); + + return $rows; + } + + /** + * Remove all duplicate rows for array considering array keys and key brothers + * @todo refactor this function to more elegant and faster + * + * @param $array + * @return array + */ + protected function removeDuplicates($array) { + $output = []; + foreach ($array As $key => $value) { + foreach ($value As $key_i => $value_i) { + $value_old = $value_i[0]; + $all_equal = true; + if(is_array(($value_i))) { + for($j =0; $j < count($value_i); $j++){ + if ($value_i[$j] !== $value_old) { + $all_equal = false; + break; + } + $value_old = $value_i[$j]; + } + } else { + $value_old = $value_i; + } + + $delimiter = ''; + $key_delimiter = ''; + if(strpos($key_i, '>') !== false){ //datacite + $delimiter = '>'; + $key_delimiter = '@'; + } else if( strpos($key_i, 'dc') !== false ) { //dcc + $delimiter = '@'; + } + + $brothers = $this->getBrothersKey($key_i, $value, $delimiter, $key_delimiter); + + if($all_equal && empty($brothers)) { // all values equals and without brother(s) + $output[$key][$key_i] = $value_old; + + } else if(!$all_equal && empty($brothers)) { // not all values are equals and without brother(s) for dc, dcc cases + for($j =0; $j < count($value_i); $j++){ + if (!in_array($value_i[$j], $output[$key][$key_i])) { + $output[$key][$key_i][] = $value_i[$j]; + } + } + + } else { // has brother key + $tuples = []; + $m = 0; + $nChildren = count($brothers[0][key($brothers[0])]); + for ($k = 0; $k < $nChildren; $k++) { + $tuple = ""; + for ($l = 0; $l < count($brothers); $l++) { + if(is_array($brothers[$l][key($brothers[$l])])){ + $tuple = $tuple . $brothers[$l][key($brothers[$l])][$m]; + } else { + $tuple = $tuple . $brothers[$l][key($brothers[$l])]; + } + } + if(!in_array($tuple, $tuples)) { + if(is_array($array[$key][$key_i])){ + $output[$key][$key_i][] = $array[$key][$key_i][$m]; + } else { + $output[$key][$key_i][] = $array[$key][$key_i]; + } + } + $m++; + $tuples[] = $tuple; + } + } + } + } + + return $output; + } + + /** + * Get all brothers for a key in an array + * + * @param $key + * @param $array + * @param $delimiter + * e.g. > + * @param $key_delimiter + * e.g. @ + * + * @return array + */ + public function getBrothersKey($key, $array, $delimiter, $key_delimiter) { + $parts = explode($delimiter, $key); + $sub_key = ""; + $output = []; + + if($delimiter === ">") { // datacite + if(count($parts) <= 1) { + return $output; + } + if(count($parts) < 3 || strpos($key, $key_delimiter) !== false) { + return $output; + } + // Compose family key (sub_key) + for($i = 0; $i < count($parts)-1; $i++){ + if($sub_key === ""){ + $sub_key = $parts[$i]; + } else { + $sub_key = $sub_key . $delimiter . $parts[$i]; + } + } + } else { // dcc + // Only one sub_key + $sub_key = $parts[0]; + } + + foreach ($array as $key => $value){ + if (strpos($key, $sub_key) !== false) { + $output[] = [$key => $value]; + } + } + + // Remove if has only one value + if(count($output) === 1) { + $output = []; + } + + return $output; + } + + /** + * Return a formatted row value in array with all values as cartesian product of rows + * + * @param ResultRow $row + * + * @return array + * + */ + protected function populateRow($row_id, ResultRow $row): array { + $output = []; + + foreach ($this->view->field as $id => $field) { + try { + $value = $this->view->style_plugin->getField($row_id, $id); + + if ($field->option['hide_empty'] && empty($value)) { + continue; + } + + if(isset($field->option['type']) && $field->options['type'] == "datetime_default") { + $value = \Drupal::service('date.formatter')->format( + strtotime($value), $field->options['settings']['format_type'] + ); + } + } + catch (\TypeError $e) { + // If relations are NULL's. + $value = false; + } + catch (\InvalidArgumentException $e) { + // If an invalid value was passed to format() + $value = false; + } + if (($alias = $this->getFieldKeyAlias($id)) && $value) { + if (array_key_exists($alias, $output)) { + $output[$alias] = $this->convert($output[$alias], $value); + } + else { + $output[$alias] = $value; + } + } + + } + + return $output; + } + + /** + * Get alias for a key in fields list + * + * @param $id + * + * @return array|null + */ + protected function getFieldKeyAlias($id) { + $fields = $this->options['field_mappings'][$this->displayHandler->getCurrentMetadataPrefix()]; + + if (isset($fields) && isset($fields[$id]) && $fields[$id] !== 'none') { + return $fields[$id]; + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['request_format']; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return ['views_oai_pmh']; + } + + /** + * Get plugin entity for some plugin id + * + * @param $plugin_id + * + * @return \Drupal\views_oai_pmh\Plugin\MetadataPrefixInterface + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + protected function getInstancePlugin($plugin_id): MetadataPrefixInterface { + if (isset($this->pluginInstances[$plugin_id])) { + return $this->pluginInstances[$plugin_id]; + } + $this->pluginInstances[$plugin_id] = $this->prefixManager->createInstance($plugin_id); + + return $this->pluginInstances[$plugin_id]; + } + +} diff --git a/src/Service/FormatRowToXml.php b/src/Service/FormatRowToXml.php new file mode 100644 index 0000000..1b64d84 --- /dev/null +++ b/src/Service/FormatRowToXml.php @@ -0,0 +1,257 @@ + nValues] + */ + protected $tagsPrefixedWith0 = []; + + + /** + * Reset tagsPrefixedWith0 value + */ + public function resetTagsPrefixedWith0(){ + $this->tagsPrefixedWith0 = []; + } + + /** + * Compose end values + * + * @param $value + * + * @return array + */ + protected function buildValues($value) { + $output = []; + if (is_array($value)) { + $i = 0; $eval_output = false; + foreach ($value as $items) { + if (is_array($items)) { + if (count($items) === 1) { + $end = $items[0]; + } else { + $end = $items; + $j = $i; + foreach ($items AS $item) { + $output[" $j"] = [ + '#' => $item, + ]; + $j++; + } + $eval_output = true; + } + } else { + $end = $items; + } + if(!$eval_output){ + $output[" $i"] = [ + '#' => $end, + ]; + } + $i++; + } + } + else { + $output['#'] = $value; + } + + return $output; + } + + /** + * Compose attribute to set + * + * @param $attribute_name + * tag name. e.g "descriptionType" + * + * @param $attribute_value + * tag value. e.g "Abstract" + * + * @param $value + * tag composed value + * + * @return array + * Current attribute to set + * + */ + protected function buildAttributes($attribute_name, $attribute_value, $value) { + $output = []; + if (is_array($attribute_value) && !array_key_exists('#', $value)) { + foreach ($attribute_value as $id_row => $val_row) { + if(is_numeric($id_row)){ + $output[' ' . $id_row]['@' . $attribute_name] = $val_row; + } else { + $output[$id_row]['@' . $attribute_name] = $val_row; + } + } + } + else { + $output['@' . $attribute_name] = $attribute_value; + } + + return $output; + } + + /** + * Transform views rows data to array composer for xml for this view row + * + * @param array $row + * view row data + * + * @return array + * array ready to convert to xml + */ + public function transform(array $row): array { + $values = []; + $output = []; + + foreach ($row as $alias => $value) { + if ($attribute = $this->hasAttribute($alias)) { + $tag_name = $attribute[0]; + $tag_attr_name = $attribute[1]; + $current_value = $this->buildAttributes($tag_attr_name, $value, $values[$tag_name]); + $alias = $tag_name; + } + else { + $tag_name = $alias; + $current_value = $this->buildValues($value); + } + $values[$tag_name] = $current_value; + $output = array_merge_recursive( + $output, + $this->depth($alias, $current_value) + ); + } + $output = array_map(array($this, "trimmingKeys"), $output); + + return $output; + } + + /** + * Strip whitespace from the beginning and end of a array keys + * + * @param $array + * array to trim keys + * + * @return array + */ + public function trimmingKeys(&$array) { + if (gettype($array) === 'array') { + foreach ($array As $key => $value){ + if (strpos($key, ' ') !== false) { + $array[trim($key)] = $array[$key]; + unset($array[$key]); + } + } + return array_map(array($this, "trimmingKeys"), $array); + } + else { + return $array; + } + } + + /** + * Process a field data and return array composed for xml for this field + * + * @param $alias + * field name on view. + * e.g. titles>title, + * + * @param $value + * array that contains the value. + * e.g. + * [ + * ['#'] = 'University of Mexico'] + * ] + * + * @return array + * array composed for xml in depth. + * e.g. + * [ + * [titles] = [ + * [title] = [ + * [#] 'University of Mexico' + * ] + * ] + * ] + */ + protected function depth($alias, $value) { + $parts = explode('>', $alias); + if (count($parts) === 1) { // dcc, dc + return [ $alias => $value ]; + } + + // datacite + $output = []; $end_content = []; $n_values = count($value); $n_parts = count($parts); + + $end_at_pre_last = ($n_parts > 2)? true:false; + + for ($i = 0; $i < $n_values; $i++) { + if ($end_at_pre_last) { + $end_content[" $i"][end($parts)] = $value[" $i"]; + } else { + $end_content[end($parts)][" $i"] = $value[" $i"]; + } + } + + $i = 0; + foreach (array_reverse($parts) as $part){ + if (count($output) == 0 && $n_values == 1 && !array_key_exists($alias, $this->tagsPrefixedWith0)) { + if (strpos(key($value), "@") !== false || !empty($value['#'])) { + if ($end_at_pre_last) { + $output[" 0"][$part] = $value; + } else { + $output[$part][" 0"] = $value; + } + } else { + if ($end_at_pre_last) { + $output[" 0"][$part] = $value[" 0"]; + } else { + $output[$part][" 0"] = $value[" 0"]; + } + } + } else if ( ($n_values > 1 && $i === 0) || ($n_values === 1 && $i === 0 && array_key_exists($alias, $this->tagsPrefixedWith0))) { // last element + $i++; + } else if ( $n_values > 1 && $i == 1 ) { // pre-last element + if ($end_at_pre_last) { + $this->tagsPrefixedWith0 += array("$alias" => $n_values); + for($j = 0; $j < count($end_content); $j++) { + $output[$part][" $j"] = $end_content[" $j"]; + } + } else { // pre-last is the end + $output[$part] = $end_content; + } + $i++; + } else if ( $n_values === 1 && $i == 1 && array_key_exists($alias, $this->tagsPrefixedWith0) ) { // pre-last element + for($j = 0; $j < $this->tagsPrefixedWith0[$alias]; $j++){ + $output[$part][" $j"][end($parts)] = $value; + } + $i++; + } else { // other elements + $last_key = key($output); + $tmp = $output[$last_key]; + array_pop($output); + $output[$part][$last_key] = $tmp; + } + } + return $output; + } + + /** + * @param $alias + * @return array|bool + */ + protected function hasAttribute($alias) { + $att = explode('@', $alias); + if (count($att) > 1) { + return $att; + } + + return FALSE; + } + +} diff --git a/src/Service/Provider.php b/src/Service/Provider.php new file mode 100644 index 0000000..5e21688 --- /dev/null +++ b/src/Service/Provider.php @@ -0,0 +1,27 @@ +getResponse()->getBody()->getContents(); + } + +} diff --git a/src/Service/PsrHttpFactory.php b/src/Service/PsrHttpFactory.php new file mode 100644 index 0000000..ff192c2 --- /dev/null +++ b/src/Service/PsrHttpFactory.php @@ -0,0 +1,24 @@ +createRequest($stack->getCurrentRequest()); + } + +} diff --git a/src/Service/Repository.php b/src/Service/Repository.php new file mode 100644 index 0000000..54595e3 --- /dev/null +++ b/src/Service/Repository.php @@ -0,0 +1,225 @@ +get('system.site'); + + $this->siteName = $system_site + ->getOriginal('name', FALSE); + $this->mail = $system_site->get('mail'); + + $currentRequest = $request->getCurrentRequest(); + + $this->host = $currentRequest->getHost(); + $this->path = $currentRequest->getPathInfo(); + $this->scheme = $currentRequest->getScheme() . '://'; + $this->port = ':' . $currentRequest->getPort(); + } + + /** + * + */ + public function getBaseUrl() { + return $this->scheme . $this->host . $this->port . $this->path; + } + + /** + * + */ + public function getGranularity() { + return 'YYYY-MM-DDThh:mm:ssZ'; + } + + /** + * + */ + public function identify() { + $description = new \DOMDocument(); + $oai_identifier = $description->createElement('oai-identifier'); + + $oai_identifier->setAttribute('xmlns', 'http://www.openarchives.org/OAI/2.0/oai-identifier'); + $oai_identifier->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + $oai_identifier->setAttribute('xsi:schemaLocation', 'http://www.openarchives.org/OAI/2.0/oai-identifier http://www.openarchives.org/OAI/2.0/oai-identifier.xsd'); + $description->appendChild($oai_identifier); + + return new Identity( + $this->siteName, + new \DateTime(), + 'transient', + [ + $this->mail, + ], + $this->getGranularity(), + 'deflate', + $description + ); + } + + /** + * + */ + public function listSets() { + throw new NoRecordsMatchException('This repository does not support sets.'); + } + + /** + * + */ + public function listSetsByToken($token) { + throw new NoRecordsMatchException('This repository does not support sets.'); + } + + /** + * + */ + public function getRecord($metadataFormat, $identifier) { + if (!isset($this->records[$identifier])) { + throw new NoRecordsMatchException("Record with identifier '{$identifier}' does not exist"); + } + return $this->records[$identifier]; + } + + /** + * + */ + public function listRecords($metadataFormat = NULL, \DateTime $from = NULL, \DateTime $until = NULL, $set = NULL) { + $token = NULL; + if ($this->offset) { + $token = $this->encodeResumptionToken($this->offset, $from, $until, $metadataFormat, $set); + } + + return new RecordList($this->records, $token); + } + + /** + * Get resumption token + * + * @param int $offset + * @param DateTime $from + * @param DateTime $util + * @param string $metadataPrefix + * @param string $set + * @return string + */ + private function encodeResumptionToken($offset = 0, \DateTime $from = null, \DateTime $util = null, $metadataPrefix = null, $set = null) { + $params = []; + $params['offset'] = $offset; + $params['metadataPrefix'] = $metadataPrefix; + $params['set'] = $set; + $params['from'] = null; + $params['until'] = null; + + if ($from) { + $params['from'] = $from->getTimestamp(); + } + + if ($util) { + $params['until'] = $util->getTimestamp(); + } + + return base64_encode(json_encode($params)); + } + + /** + * + */ + public function listRecordsByToken($token) { + $params = $this->decodeResumptionToken($token); + $token = NULL; + if ($this->offset) { + $token = $this->encodeResumptionToken($this->offset, NULL, NULL, $params['metadataPrefix']); + } + return new RecordList($this->records, $token); + } + + /** + * Decode resumption token + * possible properties are: + * + * ->offset + * ->metadataPrefix + * ->set + * ->from (timestamp) + * ->until (timestamp) + * + * @param string $token + * @return array + */ + public function decodeResumptionToken($token) { + $params = (array) json_decode(base64_decode($token)); + + if (!empty($params['from'])) { + $params['from'] = new \DateTime('@' . $params['from']); + } + + if (!empty($params['until'])) { + $params['until'] = new \DateTime('@' . $params['until']); + } + + return $params; + } + + /** + * + */ + public function listMetadataFormats($identifier = NULL) { + return $this->formats; + } + + /** + * + */ + public function setRecords(array $records) { + $this->records = $records; + } + + /** + * + */ + public function setMetadataFormats(array $formats) { + $this->formats = $formats; + } + + public function setOffset(int $offset) { + $this->offset = $offset; + } + + public function setTotalRecords(int $total) { + $this->totalRecords = $total; + } + +} diff --git a/src/Service/ValueConvertTrait.php b/src/Service/ValueConvertTrait.php new file mode 100644 index 0000000..45952bd --- /dev/null +++ b/src/Service/ValueConvertTrait.php @@ -0,0 +1,36 @@ + $value) { + $output[] = $value; + } + } + else { + $output[] = $currentValue; + } + + $output[] = $newValue; + + return $output; + } + +} diff --git a/views_oai_pmh.info.yml b/views_oai_pmh.info.yml new file mode 100644 index 0000000..76495a5 --- /dev/null +++ b/views_oai_pmh.info.yml @@ -0,0 +1,8 @@ +name: Views OAI-PMH +type: module +description: Views plugins to publish data through the OAI-PMH protocol. +core_version_requirement: ^8.7.7 || ^9 || 10 +package: Views +dependencies: + - drupal:views + - drupal:serialization diff --git a/views_oai_pmh.services.yml b/views_oai_pmh.services.yml new file mode 100644 index 0000000..78608f8 --- /dev/null +++ b/views_oai_pmh.services.yml @@ -0,0 +1,26 @@ +services: + plugin.manager.views_oai_pmh_prefix: + class: Drupal\views_oai_pmh\Plugin\MetadataPrefixManager + parent: default_plugin_manager + + views_oai_pmh.format_row_xml: + class: Drupal\views_oai_pmh\Service\FormatRowToXml + + views_oai_pmh.repository: + class: Drupal\views_oai_pmh\Service\Repository + arguments: + - '@config.factory' + - '@request_stack' + + views_oai_pmh.provider: + class: Drupal\views_oai_pmh\Service\Provider + arguments: ['@views_oai_pmh.repository', '@views_oai_pmh.psr7_request'] + + views_oai_pmh.psr7_request_factory: + class: Drupal\views_oai_pmh\Service\PsrHttpFactory + + views_oai_pmh.psr7_request: + class: Zend\Diactoros\ServerRequest + factory: ['@views_oai_pmh.psr7_request_factory', createDiactorosFactory] + arguments: + - '@request_stack'