diff --git a/config/vufind/EDS.ini b/config/vufind/EDS.ini index f2a50448b8145fed98d9bbcda410f961e586e5a0..07bf4aa9ecdaf50413d67c0c41b255624e569a98 100644 --- a/config/vufind/EDS.ini +++ b/config/vufind/EDS.ini @@ -212,3 +212,24 @@ organization_id = "VuFind 2.x from MyUniversity" [List] view=full +; This section controls the behavior of the Autocomplete of EDS +; If enabled the option "autocomplete" is send for UIDAuth to get the token +; and the url. +[Autocomplete] +; Set this to false to disable all autocomplete behavior +enabled = true + +; Define a default_handler +default_handler = Eds + +; In this section, set the key equal to a search type from [Basic_Searches] and +; the value equal to an autocomplete handler in order to customize autocompletion +; behavior when that search type is selected. (default: Eds:rawqueries) +; These values are available: None, Eds:rawqueries and Eds:holdings +; Use None to disable autocomplete for a specific search type +; Use Eds:holdings for title completion in PubFinder. +; Use Eds:rawqueries for completion of basic textual queries. +[Autocomplete_Types] +;AllFields = Eds:rawqueries +;TI = Eds:holdings +AU = None diff --git a/module/VuFind/src/VuFind/Autocomplete/Eds.php b/module/VuFind/src/VuFind/Autocomplete/Eds.php new file mode 100644 index 0000000000000000000000000000000000000000..18094b556eb7d3e153b7faa4fa1ae0e89fd33ee2 --- /dev/null +++ b/module/VuFind/src/VuFind/Autocomplete/Eds.php @@ -0,0 +1,116 @@ +<?php +/** + * EDS Autocomplete Module + * + * PHP version 7 + * + * Copyright (C) Villanova University 2018. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Autocomplete + * @author Demian Katz <demian.katz@villanova.edu> + * @author Chris Hallberg <challber@villanova.edu> + * @author Jochen Lienhard <jochen.lienhard@ub.uni-freiburg.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:autosuggesters Wiki + */ +namespace VuFind\Autocomplete; + +/** + * EDS Autocomplete Module + * + * This class provides popular terms provided by EDS. + * + * @category VuFind + * @package Autocomplete + * @author Demian Katz <demian.katz@villanova.edu> + * @author Jochen Lienhard <jochen.lienhard@ub.uni-freiburg.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:autosuggesters Wiki + */ +class Eds implements AutocompleteInterface +{ + /** + * Eds domain + * + * @var string + */ + protected $domain = 'rawqueries'; + + /** + * Search object family to use + * + * @var string + */ + protected $searchClassId = 'EDS'; + + /** + * Results plugin manager + * + * @var \VuFindSearch\Backend\EDS\Backend + */ + protected $backend; + + /** + * Constructor + * + * @param \VuFindSearch\Backend\EDS\Backend $backend Results plugin manager + */ + public function __construct(\VuFindSearch\Backend\EDS\Backend $backend) + { + $this->backend = $backend; + } + + /** + * This method returns an array of strings matching the user's query for + * display in the autocomplete box. + * + * @param string $query The user query + * + * @return array The suggestions for the provided query + */ + public function getSuggestions($query) + { + // Initialize return array: + $results = []; + + if (!is_object($this->backend)) { + throw new \Exception('Please set configuration first.'); + } + + try { + // Perform the autocomplete search: + $results = $this->backend->autocomplete($query, $this->domain); + } catch (\Exception $e) { + // Ignore errors -- just return empty results if we must. + } + return isset($results) ? array_unique($results) : []; + } + + /** + * Set parameters that affect the behavior of the autocomplete handler. + * These values normally come from the EDS configuration file. + * + * @param string $params Parameters to set + * + * @return void + */ + public function setConfig($params) + { + // Only change the value if it is not empty: + $this->domain = !empty($params) ? $params : $this->domain; + } +} diff --git a/module/VuFind/src/VuFind/Autocomplete/EdsFactory.php b/module/VuFind/src/VuFind/Autocomplete/EdsFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..1dbe0ef91e05212fe46217ee73b17e19d7b72dbc --- /dev/null +++ b/module/VuFind/src/VuFind/Autocomplete/EdsFactory.php @@ -0,0 +1,68 @@ +<?php +/** + * Factory for EDS-driven autocomplete plugins. Works for \VuFind\Autocomplete\Eds + * + * PHP version 7 + * + * Copyright (C) Villanova University 2017. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Autocomplete + * @author Demian Katz <demian.katz@villanova.edu> + * @author Jochen Lienhard <jochen.lienhard@ub.uni-freiburg.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\Autocomplete; + +use Interop\Container\ContainerInterface; + +/** + * Factory for EDS-driven autocomplete plugins. Works for \VuFind\Autocomplete\Eds + * + * @category VuFind + * @package Autocomplete + * @author Demian Katz <demian.katz@villanova.edu> + * @author Jochen Lienhard <jochen.lienhard@ub.uni-freiburg.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class EdsFactory implements \Zend\ServiceManager\Factory\FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + return new $requestedName( + $container->get('VuFind\Search\BackendManager')->get('EDS') + ); + } +} diff --git a/module/VuFind/src/VuFind/Autocomplete/PluginManager.php b/module/VuFind/src/VuFind/Autocomplete/PluginManager.php index 75c013f04a14def27176d4b33f9542b83eb2691c..748c4ba3d56051717ca3ae3d0a99e77f0ecb275e 100644 --- a/module/VuFind/src/VuFind/Autocomplete/PluginManager.php +++ b/module/VuFind/src/VuFind/Autocomplete/PluginManager.php @@ -45,6 +45,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager */ protected $aliases = [ 'none' => 'VuFind\Autocomplete\None', + 'eds' => 'VuFind\Autocomplete\Eds', 'oclcidentities' => 'VuFind\Autocomplete\OCLCIdentities', 'search2' => 'VuFind\Autocomplete\Search2', 'search2cn' => 'VuFind\Autocomplete\Search2CN', @@ -70,6 +71,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager */ protected $factories = [ 'VuFind\Autocomplete\None' => 'Zend\ServiceManager\Factory\InvokableFactory', + 'VuFind\Autocomplete\Eds' => 'VuFind\Autocomplete\EdsFactory', 'VuFind\Autocomplete\OCLCIdentities' => 'Zend\ServiceManager\Factory\InvokableFactory', 'VuFind\Autocomplete\Search2' => 'VuFind\Autocomplete\SolrFactory', diff --git a/module/VuFind/src/VuFind/Search/EDS/Options.php b/module/VuFind/src/VuFind/Search/EDS/Options.php index 6d8726a521150efdd5db4d09e616ec877a02f25c..b14e00526eb0542f71e8767caa12c4b90722739d 100644 --- a/module/VuFind/src/VuFind/Search/EDS/Options.php +++ b/module/VuFind/src/VuFind/Search/EDS/Options.php @@ -137,6 +137,10 @@ class Options extends \VuFind\Search\Base\Options $facetConf->Advanced_Facet_Settings->translated_facets->toArray() ); } + // Load autocomplete preference: + if (isset($searchSettings->Autocomplete->enabled)) { + $this->autocompleteEnabled = $searchSettings->Autocomplete->enabled; + } } /** diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/EdsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/EdsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a9ec3abaa33914160df1525d96358c09f2884f0d --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/EdsTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Solr autocomplete test class. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2018. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @author Jochen Lienhard <jochen.lienhard@ub.uni-freiburg.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFindTest\Autocomplete; + +use VuFind\Autocomplete\Eds; + +/** + * Eds autocomplete test class. + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @author Jochen Lienhard <jochen.lienhard@ub.uni-freiburg.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class EdsTest extends \VuFindTest\Unit\TestCase +{ + /** + * Get a mock backend + * + * @param string $id ID of fake backend. + * + * @return \VuFindSearch\Backend\EDS\Backend + */ + protected function getMockBackend($id = 'EDS') + { + return $this->getMockBuilder('VuFindSearch\Backend\EDS\Backend') + ->setMethods(['autocomplete']) + ->disableOriginalConstructor()->getMock(); + } + + /** + * Test getSuggestions. + * + * @return void + */ + public function testGetSuggestions() + { + $backend = $this->getMockBackend(); + $eds = new Eds($backend); + $backend->expects($this->once()) + ->method('autocomplete') + ->with($this->equalTo('query'), $this->equalTo('rawqueries')) + ->will($this->returnValue([1, 2, 3])); + $this->assertEquals([1, 2, 3], $eds->getSuggestions('query')); + } + + /** + * Test getSuggestions with non-default configuration. + * + * @return void + */ + public function testGetSuggestionsWithNonDefaultConfiguration() + { + $backend = $this->getMockBackend(); + $eds = new Eds($backend); + $eds->setConfig('holdings'); + $backend->expects($this->once()) + ->method('autocomplete') + ->with($this->equalTo('query'), $this->equalTo('holdings')) + ->will($this->returnValue([4, 5])); + $this->assertEquals([4, 5], $eds->getSuggestions('query')); + } +} diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php index 1e9f17fbae883c7b0d3c36d92459eb2f40e72824..e22d9da23123fe5fe9898c316cb4bb333a161662 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php @@ -384,6 +384,20 @@ class Backend extends AbstractBackend $this->queryBuilder = $queryBuilder; } + /** + * Get popular terms using the autocomplete API. + * + * @param string $query Simple query string + * @param string $domain Autocomplete type (e.g. 'rawqueries' or 'holdings') + * + * @return array of terms + */ + public function autocomplete($query, $domain = 'rawqueries') + { + return $this->client + ->autocomplete($query, $domain, $this->getAutocompleteData()); + } + /// Internal API /** @@ -449,6 +463,59 @@ class Backend extends AbstractBackend return $token; } + /** + * Obtain the autocomplete authentication to use with the EDS API from cache + * if it exists. If not, then generate a new set. + * + * @param bool $isInvalid whether or not the the current autocomplete data + * is invalid and should be regenerated + * + * @return array autocomplete data + */ + protected function getAutocompleteData($isInvalid = false) + { + // Autocomplete is currently unsupported with IP authentication + if ($this->ipAuth) { + return null; + } + if ($isInvalid) { + $this->cache->setItem('edsAutocomplete', null); + } + $autocompleteData = $this->cache->getItem('edsAutocomplete'); + if (!empty($autocompleteData)) { + $currentToken = $autocompleteData['token'] ?? ''; + $expirationTime = $autocompleteData['expiration'] ?? 0; + + // Check to see if the token expiration time is greater than the current + // time. If the token is expired or within 5 minutes of expiring, + // generate a new one. + if (!empty($currentToken) && (time() <= ($expirationTime - (60 * 5)))) { + return $autocompleteData; + } + } + + $username = $this->userName; + $password = $this->password; + if (!empty($username) && !empty($password)) { + $results = $this->client + ->authenticate($username, $password, $this->orgId, ['autocomplete']); + $autoresult = $results['Autocomplete'] ?? []; + if (isset($autoresult['Token']) && isset($autoresult['TokenTimeOut']) + && isset($autoresult['CustId']) && isset($autoresult['Url']) + ) { + $token = $autoresult['Token']; + $expiration = $autoresult['TokenTimeOut'] + time(); + $custid = $autoresult['CustId']; + $url = $autoresult['Url']; + + $autocompleteData = compact('token', 'expiration', 'url', 'custid'); + // store token, expiration, url and custid in cache. + $this->cache->setItem('edsAutocomplete', $autocompleteData); + } + } + return $autocompleteData; + } + /** * Print a message if debug is enabled. * diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php index 5092e241b4da7e5e4ffebf96a0e657ca06add068..344c442c00bf1b28a009f493bd6e1cbf255e33e7 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php @@ -210,17 +210,61 @@ abstract class Base return $this->call($url, $headers, $qs); } + /** + * Parse autocomplete response from API in an array of terms + * + * @param array $msg Response from API + * + * @return array of terms + */ + protected function parseAutocomplete($msg) + { + $result = []; + if (isset($msg["terms"]) && is_array($msg["terms"])) { + foreach ($msg["terms"] as $value) { + $result[] = $value["term"]; + } + } + return $result; + } + + /** + * Execute an EdsApi autocomplete + * + * @param string $query Search term + * @param string $type Autocomplete type (e.g. 'rawqueries' or 'holdings') + * @param array $data Autocomplete API details (from authenticating with + * 'autocomplete' option set -- requires token, custid and url keys). + * @param bool $raw Should we return the results raw (true) or processed + * (false)? + * + * @return array An array of autocomplete terns as returned from the api + */ + public function autocomplete($query, $type, $data, $raw = false) + { + // build request + $url = $data['url'] . '?idx=' . urlencode($type) . + '&token=' . urlencode($data['token']) . + '&filters=[{"name"%3A"custid"%2C"values"%3A["' . + urlencode($data['custid']) . '"]}]&term=' . urlencode($query); + $this->debugPrint("Autocomplete URL: " . $url); + $response = $this->call($url, null, null, 'GET', null); + return $raw ? $response : $this->parseAutocomplete($response); + } + /** * Generate an authentication token with a valid EBSCO EDS Api account * * @param string $username username associated with an EBSCO EdsApi account * @param string $password password associated with an EBSCO EdsApi account * @param string $orgid Organization id the request is initiated from + * @param array $params optional params (autocomplete) * * @return array */ - public function authenticate($username = null, $password = null, $orgid = null) - { + public function authenticate($username = null, $password = null, + $orgid = null, $params = null + ) { $this->debugPrint( "Authenticating: username: $username, password: XXXXXXX, orgid: $orgid" ); @@ -236,6 +280,9 @@ abstract class Base if (isset($org)) { $authInfo['orgid'] = $org; } + if (isset($params)) { + $authInfo['Options'] = $params; + } $messageBody = json_encode($authInfo); return $this->call($url, null, null, 'POST', $messageBody); } diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/eds/response/autocomplete b/module/VuFindSearch/tests/unit-tests/fixtures/eds/response/autocomplete new file mode 100644 index 0000000000000000000000000000000000000000..cc7a47cb0b5ce1dd6bc6e1002f0ceaf2772f67e9 --- /dev/null +++ b/module/VuFindSearch/tests/unit-tests/fixtures/eds/response/autocomplete @@ -0,0 +1 @@ +a:3:{s:4:"type";s:20:"autoCompleteResponse";s:14:"processingTime";i:1;s:5:"terms";a:10:{i:0;a:4:{s:4:"term";s:5:"black";s:15:"highlightedTerm";s:74:"<span class="userprovided">bla</span><span class="autocompleted">ck</span>";s:5:"score";i:16295;s:6:"domain";s:10:"rawqueries";}i:1;a:4:{s:4:"term";s:18:"black lives matter";s:15:"highlightedTerm";s:87:"<span class="userprovided">bla</span><span class="autocompleted">ck lives matter</span>";s:5:"score";i:11493;s:6:"domain";s:10:"rawqueries";}i:2;a:4:{s:4:"term";s:11:"black women";s:15:"highlightedTerm";s:80:"<span class="userprovided">bla</span><span class="autocompleted">ck women</span>";s:5:"score";i:6295;s:6:"domain";s:10:"rawqueries";}i:3;a:4:{s:4:"term";s:10:"black hole";s:15:"highlightedTerm";s:79:"<span class="userprovided">bla</span><span class="autocompleted">ck hole</span>";s:5:"score";i:3387;s:6:"domain";s:10:"rawqueries";}i:4;a:4:{s:4:"term";s:11:"black death";s:15:"highlightedTerm";s:80:"<span class="userprovided">bla</span><span class="autocompleted">ck death</span>";s:5:"score";i:3371;s:6:"domain";s:10:"rawqueries";}i:5;a:4:{s:4:"term";s:14:"black panthers";s:15:"highlightedTerm";s:83:"<span class="userprovided">bla</span><span class="autocompleted">ck panthers</span>";s:5:"score";i:3148;s:6:"domain";s:10:"rawqueries";}i:6;a:4:{s:4:"term";s:9:"black men";s:15:"highlightedTerm";s:78:"<span class="userprovided">bla</span><span class="autocompleted">ck men</span>";s:5:"score";i:2205;s:6:"domain";s:10:"rawqueries";}i:7;a:4:{s:4:"term";s:27:"black lives matter movement";s:15:"highlightedTerm";s:96:"<span class="userprovided">bla</span><span class="autocompleted">ck lives matter movement</span>";s:5:"score";i:2117;s:6:"domain";s:10:"rawqueries";}i:8;a:4:{s:4:"term";s:19:"black panther party";s:15:"highlightedTerm";s:88:"<span class="userprovided">bla</span><span class="autocompleted">ck panther party</span>";s:5:"score";i:2016;s:6:"domain";s:10:"rawqueries";}i:9;a:4:{s:4:"term";s:12:"black plague";s:15:"highlightedTerm";s:81:"<span class="userprovided">bla</span><span class="autocompleted">ck plague</span>";s:5:"score";i:1367;s:6:"domain";s:10:"rawqueries";}}} diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php index 2985275d6ae439abebd60b943ab681ca759e006e..bb9a13e053c3939696ca4cee0ee8c9eefbb1f1c9 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php @@ -43,6 +43,39 @@ use VuFindSearch\Query\Query; */ class BackendTest extends \VuFindTest\Unit\TestCase { + /** + * Test performing an autocomplete + * + * @return void + */ + public function testAutocomplete() + { + $conn = $this->getConnectorMock(['call']); + $expectedUri = 'http://foo?idx=rawdata&token=auth1234' + . '&filters=[{"name"%3A"custid"%2C"values"%3A["foo"]}]&term=bla'; + $conn->expects($this->once()) + ->method('call') + ->with($this->equalTo($expectedUri)) + ->will($this->returnValue($this->loadResponse('autocomplete'))); + + $back = $this->getBackend( + $conn, $this->getRCFactory(), null, null, [], ['getAutocompleteData'] + ); + $autocompleteData = [ + 'custid' => 'foo', 'url' => 'http://foo', 'token' => 'auth1234' + ]; + $back->expects($this->any()) + ->method('getAutocompleteData') + ->will($this->returnValue($autocompleteData)); + + $coll = $back->autocomplete('bla', 'rawdata'); + // check count + $this->assertCount(10, $coll); + foreach ($coll as $value) { + $this->assertEquals('bla', substr($value, 0, 3)); + } + } + /** * Test retrieving a record. *