Skip to content

Commit

Permalink
Generalize DOI linker system to support other identifier types (#3918)
Browse files Browse the repository at this point in the history
  • Loading branch information
demiankatz authored Feb 21, 2025
1 parent 5465f4b commit 46a14f3
Show file tree
Hide file tree
Showing 49 changed files with 1,130 additions and 878 deletions.
1 change: 0 additions & 1 deletion .eslintrc.jsdoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ module.exports = {
"themes/bootstrap5/js/openurl.js",
"themes/bootstrap5/js/ill.js",
"themes/bootstrap5/js/record.js",
"themes/bootstrap5/js/doi.js",
"themes/bootstrap5/js/keep_alive.js",
"themes/bootstrap5/js/embedGBS.js",
"themes/bootstrap5/js/lib/ajax_request_queue.js",
Expand Down
18 changes: 12 additions & 6 deletions config/vufind/BrowZine.ini
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ load_results_with_js = true
; If you want to disable skipping of generic images, set the value to an empty string.
ignored_images[] = "https://assets.thirdiron.com/default-journal-cover.png"

; This section controls the behavior of the BrowZine DOI handler; see also
; the [DOI] section of config.ini to activate the handler.
[DOI]
; This section controls the behavior of the BrowZine identifier link handler; see also
; the [IdentifierLinks] section of config.ini to activate the handler.
[IdentifierLinks]
; Can be set to "include" to include only the options listed in the "filter"
; setting, or to "exclude" to filter out the options listed in the "filter"
; setting. The default is "none," which performs no filtering.
;filterType = exclude
; This repeatable section can be used to filter based on link type; legal
; options are the keys defined in DOIServices section.
; options are the keys defined in DOIServices and ISSNServices sections.
; Note that this setting is available for convenience and compatibility with previous
; versions. You can get the same results by changing the DOIServices section.
; versions. You can get the same results by changing the DOIServices and/or
; ISSNServices sections.
;filter[] = "browzineWebLink"
;filter[] = "fullTextFile"

Expand All @@ -57,7 +58,7 @@ ignored_images[] = "https://assets.thirdiron.com/default-journal-cover.png"
; used. Default is false.
local_icons = false

; This section defines the services to display from BrowZine and their display order.
; This section defines the DOI services to display from BrowZine and their display order.
; Each key is a service name (see https://thirdiron.atlassian.net/wiki/x/WIDqAw for
; more information) and value contains translation key, local icon and Third Iron's
; icon asset (optional) separated by a pipe character.
Expand All @@ -66,6 +67,11 @@ browzineWebLink = "View Complete Issue|browzine-issue|https://assets.thirdiron.c
fullTextFile = "PDF Full Text|browzine-pdf|https://assets.thirdiron.com/images/integrations/browzine-pdf-download-icon.svg"
retractionNoticeUrl = "View Retraction Notice|browzine-retraction"

; This section defines the ISSN services to display from BrowZine and their display order.
; The format is the same as [DOIServices] above.
[ISSNServices]
browzineWebLink = "Browse Available Issues|browzine-issue|https://assets.thirdiron.com/images/integrations/browzine-open-book-icon.svg"

; This section defines the view options available on standard search results.
; If only one view is required, set default_view under [General] above, and
; leave this section commented out.
Expand Down
20 changes: 13 additions & 7 deletions config/vufind/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1425,10 +1425,10 @@ url = "https://api.booksite.com"
;[DPLA]
;apiKey = http://dp.la/info/developers/codex/policies/#get-a-key

; These settings affect dynamic DOI-based link inclusion; this can provide links
; to full text or contextual information.
[DOI]
; This setting controls whether or not DOI-based links are enabled, and which
; These settings affect dynamic identifier-based link inclusion; this can provide links
; to full text or contextual information based on identifiers like DOI, ISBN or ISSN.
[IdentifierLinks]
; This setting controls whether or not ID-based links are enabled, and which
; API is used to fetch the data. Currently supported options: BrowZine (requires
; credentials to be configured in BrowZine.ini), Demo (which generates fake data
; to simulate use of a real service, for testing), Unpaywall or false (to disable).
Expand All @@ -1437,7 +1437,7 @@ url = "https://api.booksite.com"
;resolver = BrowZine

; If you use multiple values in the resolver setting above, you can determine how the
; software should behave when multiple resolvers return results for the same DOI.
; software should behave when multiple resolvers return results for the same ID.
; You can choose "first" (only return results from the first matching resolver --
; the default behavior) or "merge" (merge together all results and show them all).
;multi_resolver_mode = first
Expand All @@ -1446,12 +1446,18 @@ url = "https://api.booksite.com"
; Unpaywall needs an email adress, see https://unpaywall.org/products/api
;unpaywall_email = "[email protected]"

; The following settings control where DOI-based links are displayed:
; The following settings control where ID-based links are displayed:
show_in_results = true ; include in search results
show_in_record = false ; include in core record metadata
show_in_holdings = false ; include in holdings tab of record view

; Whether to load any third-party icons for the DOI services via VuFind's cover
; This setting controls which types of identifiers are used to generate links; if
; the setting is omitted, all supported types (DOI, ISBN and ISSN) will be used:
supportedIdentifiers[] = doi
supportedIdentifiers[] = isbn
supportedIdentifiers[] = issn

; Whether to load any third-party icons for the ID services via VuFind's cover
; loader proxy to avoid any privacy implications. Ensure that the necessary domains
; are allowed in Content/coverproxyCache[] setting. Default is false.
proxy_icons = true
Expand Down
1 change: 1 addition & 0 deletions languages/en.ini
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Breadcrumbs = "Breadcrumbs"
Brief View = "Brief View"
Browse = "Browse"
Browse Alphabetically = "Browse Alphabetically"
Browse Available Issues = "Browse Available Issues"
Browse for Authors = "Browse for Authors"
Browse Home = "Browse Home"
Browse the Catalog = "Browse the Catalog"
Expand Down
4 changes: 2 additions & 2 deletions module/VuFind/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,6 @@
'VuFind\Db\Service\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
'VuFind\Db\Table\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
'VuFind\DigitalContent\OverdriveConnector' => 'VuFind\DigitalContent\OverdriveConnectorFactory',
'VuFind\DoiLinker\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
'VuFind\Escaper\Escaper' => 'VuFind\Escaper\EscaperFactory',
'VuFind\Export' => 'VuFind\ExportFactory',
'VuFind\Favorites\FavoritesService' => 'VuFind\Favorites\FavoritesServiceFactory',
Expand All @@ -478,6 +477,7 @@
'VuFind\Http\PhpEnvironment\Request' => 'Laminas\ServiceManager\Factory\InvokableFactory',
'VuFind\I18n\Locale\LocaleSettings' => 'VuFind\Service\ServiceWithConfigIniFactory',
'VuFind\I18n\Sorter' => 'VuFind\I18n\SorterFactory',
'VuFind\IdentifierLinker\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
'VuFind\ILS\Connection' => 'VuFind\ILS\ConnectionFactory',
'VuFind\ILS\Driver\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
'VuFind\ILS\Logic\AvailabilityStatusManager' => 'Laminas\ServiceManager\Factory\InvokableFactory',
Expand Down Expand Up @@ -701,12 +701,12 @@
'db_row' => [ /* see VuFind\Db\Row\PluginManager for defaults */ ],
'db_service' => [ /* see VuFind\Db\Service\PluginManager for defaults */ ],
'db_table' => [ /* see VuFind\Db\Table\PluginManager for defaults */ ],
'doilinker' => [ /* see VuFind\DoiLinker\PluginManager for defaults */ ],
'form_handler' => [ /* see VuFind\Form\Handler\PluginManager for defaults */],
'hierarchy_driver' => [ /* see VuFind\Hierarchy\Driver\PluginManager for defaults */ ],
'hierarchy_treedataformatter' => [ /* see VuFind\Hierarchy\TreeDataFormatter\PluginManager for defaults */ ],
'hierarchy_treedatasource' => [ /* see VuFind\Hierarchy\TreeDataSource\PluginManager for defaults */ ],
'hierarchy_treerenderer' => [ /* see VuFind\Hierarchy\TreeRenderer\PluginManager for defaults */ ],
'identifierlinker' => [ /* see VuFind\IdentifierLinker\PluginManager for defaults */ ],
'ils_driver' => [ /* See VuFind\ILS\Driver\PluginManager for defaults */ ],
'metadatavocabulary' => [ /* See VuFind\MetadataVocabulary\PluginManager for defaults */],
'navigation' => [ /* See VuFind\Navigation\PluginManager for defaults */],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

/**
* AJAX handler to look up DOI data.
* AJAX handler to look up identifier-based link data.
*
* PHP version 8
*
Expand Down Expand Up @@ -31,37 +31,30 @@

use Laminas\Mvc\Controller\Plugin\Params;
use Laminas\View\Renderer\RendererInterface;
use VuFind\DoiLinker\PluginManager;
use VuFind\IdentifierLinker\PluginManager;

use function count;

/**
* AJAX handler to look up DOI data.
* AJAX handler to look up identifier-based link data.
*
* @category VuFind
* @package AJAX
* @author Demian Katz <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/
class DoiLookup extends AbstractBase
class IdentifierLinksLookup extends AbstractBase
{
/**
* DOI Linker Plugin Manager
*
* @var PluginManager
*/
protected $pluginManager;

/**
* DOI resolver configuration value, exploded into an array of options
* Identifier link resolver configuration value, exploded into an array of options
*
* @var string[]
*/
protected $resolvers;

/**
* Behavior to use when multiple resolvers find results for the same DOI (may
* Behavior to use when multiple resolvers find results for the same identifier set (may
* be 'first' -- use first match, or 'merge' -- use all results)
*
* @var string
Expand All @@ -82,36 +75,29 @@ class DoiLookup extends AbstractBase
*/
protected $openInNewWindow = false;

/**
* View renderer
*
* @var RendererInterface
*/
protected $viewRenderer = null;

/**
* Constructor
*
* @param PluginManager $pluginManager DOI Linker Plugin Manager
* @param PluginManager $pluginManager Identifier Linker Plugin Manager
* @param RendererInterface $viewRenderer View renderer
* @param array $config Main configuration
*/
public function __construct(
PluginManager $pluginManager,
RendererInterface $viewRenderer,
protected PluginManager $pluginManager,
protected RendererInterface $viewRenderer,
array $config
) {
$this->pluginManager = $pluginManager;
// DOI config section is supported as a fallback for back-compatibility:
$idConfig = $config['IdentifierLinks'] ?? $config['DOI'] ?? [];
$this->resolvers
= array_map('trim', explode(',', $config['DOI']['resolver'] ?? ''));
= array_map('trim', explode(',', $idConfig['resolver'] ?? ''));
// Behavior to use when multiple resolvers to find results for the same
// DOI (may be 'first' -- use first match, or 'merge' -- use all
// identifier set (may be 'first' -- use first match, or 'merge' -- use all
// results):
$this->multiMode
= trim(strtolower($config['DOI']['multi_resolver_mode'] ?? 'first'));
$this->proxyIcons = !empty($config['DOI']['proxy_icons']);
$this->openInNewWindow = !empty($config['DOI']['new_window']);
$this->viewRenderer = $viewRenderer;
= trim(strtolower($idConfig['multi_resolver_mode'] ?? 'first'));
$this->proxyIcons = !empty($idConfig['proxy_icons']);
$this->openInNewWindow = !empty($idConfig['new_window']);
}

/**
Expand All @@ -123,67 +109,77 @@ public function __construct(
*/
public function handleRequest(Params $params)
{
$response = [];
$dois = (array)$params->fromQuery('doi', []);
$gatheredData = [];
$ids = json_decode($params->getController()->getRequest()->getContent(), true);
foreach ($this->resolvers as $resolver) {
if ($this->pluginManager->has($resolver)) {
$next = $this->pluginManager->get($resolver)->getLinks($dois);
$next = $this->pluginManager->get($resolver)->getLinks($ids);
$next = $this->processIconLinks($next);
foreach ($next as $doi => $data) {
foreach ($data as &$current) {
$current['newWindow'] = $this->openInNewWindow;
}
unset($current);
if (!isset($response[$doi])) {
$response[$doi] = $data;
foreach ($next as $key => $data) {
if (!isset($gatheredData[$key])) {
$gatheredData[$key] = $data;
} elseif ($this->multiMode == 'merge') {
$response[$doi] = array_merge($response[$doi], $data);
$gatheredData[$key] = array_merge($gatheredData[$key], $data);
}
}
// If all DOIs have been found and we're not in merge mode, we
// If all keys have been found and we're not in merge mode, we
// can short circuit out of here.
if (
$this->multiMode !== 'merge'
&& count(array_diff($dois, array_keys($response))) == 0
&& count(array_diff(array_keys($ids), array_keys($gatheredData))) == 0
) {
break;
}
}
}
$response = array_map([$this, 'renderResponseChunk'], $gatheredData);
return $this->formatResponse($response);
}

/**
* Proxify external DOI icon links and render local icons
* Render the links for a single record.
*
* @param array $data Data to render
*
* @return string
*/
protected function renderResponseChunk(array $data): string
{
$newWindow = $this->openInNewWindow;
return $this->viewRenderer->render('ajax/identifierLinks.phtml', compact('data', 'newWindow'));
}

/**
* Proxify external icon links and render local icons
*
* @param array $dois DOIs
* @param array $data Identifier plugin data
*
* @return array
*/
protected function processIconLinks(array $dois): array
protected function processIconLinks(array $data): array
{
$serverHelper = $this->viewRenderer->plugin('serverurl');
$urlHelper = $this->viewRenderer->plugin('url');
$iconHelper = $this->viewRenderer->plugin('icon');

foreach ($dois as &$doiLinks) {
foreach ($doiLinks as &$doi) {
if ($this->proxyIcons && !empty($doi['icon'])) {
$doi['icon'] = $serverHelper(
foreach ($data as &$links) {
foreach ($links as &$link) {
if ($this->proxyIcons && !empty($link['icon'])) {
$link['icon'] = $serverHelper(
$urlHelper(
'cover-show',
[],
['query' => ['proxy' => $doi['icon']]]
['query' => ['proxy' => $link['icon']]]
)
);
}
if (!empty($doi['localIcon'])) {
$doi['localIcon'] = $iconHelper($doi['localIcon']);
if (!empty($link['localIcon'])) {
$link['localIcon'] = $iconHelper($link['localIcon'], 'icon-link__icon');
}
}
unset($doi);
unset($link);
}
unset($doiLinks);
return $dois;
unset($links);
return $data;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

/**
* Factory for DoiLookup AJAX handler.
* Factory for IdentifierLinksLookup AJAX handler.
*
* PHP version 8
*
Expand Down Expand Up @@ -35,15 +35,15 @@
use Psr\Container\ContainerInterface;

/**
* Factory for DoiLookup AJAX handler.
* Factory for IdentifierLinksLookup AJAX handler.
*
* @category VuFind
* @package AJAX
* @author Demian Katz <[email protected]>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/
class DoiLookupFactory implements \Laminas\ServiceManager\Factory\FactoryInterface
class IdentifierLinksLookupFactory implements \Laminas\ServiceManager\Factory\FactoryInterface
{
/**
* Create an object
Expand Down Expand Up @@ -72,7 +72,7 @@ public function __invoke(
$config = $container->get(\VuFind\Config\PluginManager::class)
->get('config')->toArray();
return new $requestedName(
$container->get(\VuFind\DoiLinker\PluginManager::class),
$container->get(\VuFind\IdentifierLinker\PluginManager::class),
$container->get('ViewRenderer'),
$config
);
Expand Down
4 changes: 2 additions & 2 deletions module/VuFind/src/VuFind/AjaxHandler/PluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
'checkRequestIsValid' => CheckRequestIsValid::class,
'commentRecord' => CommentRecord::class,
'deleteRecordComment' => DeleteRecordComment::class,
'doiLookup' => DoiLookup::class,
'identifierLinksLookup' => IdentifierLinksLookup::class,
'getACSuggestions' => GetACSuggestions::class,
'getIlsStatus' => GetIlsStatus::class,
'getItemStatuses' => GetItemStatuses::class,
Expand Down Expand Up @@ -90,7 +90,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
CheckRequestIsValid::class => AbstractIlsAndUserActionFactory::class,
CommentRecord::class => CommentRecordFactory::class,
DeleteRecordComment::class => DeleteRecordCommentFactory::class,
DoiLookup::class => DoiLookupFactory::class,
IdentifierLinksLookup::class => IdentifierLinksLookupFactory::class,
GetACSuggestions::class => GetACSuggestionsFactory::class,
GetIlsStatus::class => GetIlsStatusFactory::class,
GetItemStatuses::class => GetItemStatusesFactory::class,
Expand Down
Loading

0 comments on commit 46a14f3

Please sign in to comment.