From e1678d25bacf807237f3170d0cca94c490a69d77 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Lahmann?= <lahmann@users.noreply.github.com>
Date: Wed, 8 Feb 2017 15:41:40 +0100
Subject: [PATCH] Link resolver driver improvements (#685)

- added support for datatype additionals in EZB-response for electronic journals
- added support for datatype elements in EZB-response for print journals
- added support for proper status mapping to access classes in VuFind
- extended documentation
- added tests for EZB resolver
- implemented Resolver\Driver\AbstractBase and refactored existing drivers to extend it
- added new required methods to DriverInterface
---
 .../src/VuFind/Controller/AjaxController.php  |   6 +-
 .../VuFind/Resolver/Driver/AbstractBase.php   |  87 ++++++++
 .../src/VuFind/Resolver/Driver/Demo.php       |  12 +-
 .../Resolver/Driver/DriverInterface.php       |  23 ++
 .../VuFind/src/VuFind/Resolver/Driver/Ezb.php | 203 ++++++++++++++----
 .../src/VuFind/Resolver/Driver/Redi.php       |  11 +-
 .../VuFind/src/VuFind/Resolver/Driver/Sfx.php |  11 +-
 .../VuFind/Resolver/Driver/Threesixtylink.php |  11 +-
 .../src/VuFind/View/Helper/Root/Factory.php   |   3 +
 .../src/VuFind/View/Helper/Root/OpenUrl.php   |  35 ++-
 .../tests/fixtures/resolver/response/ezb.xml  | 110 ++++++++++
 .../VuFindTest/Resolver/Driver/EzbTest.php    | 168 +++++++++++++++
 .../View/Helper/Root/OpenUrlTest.php          |   4 +-
 .../templates/Helpers/openurl.phtml           |   2 +-
 .../templates/ajax/resolverLinks.phtml        |   2 +-
 15 files changed, 609 insertions(+), 79 deletions(-)
 create mode 100644 module/VuFind/src/VuFind/Resolver/Driver/AbstractBase.php
 create mode 100644 module/VuFind/tests/fixtures/resolver/response/ezb.xml
 create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php

diff --git a/module/VuFind/src/VuFind/Controller/AjaxController.php b/module/VuFind/src/VuFind/Controller/AjaxController.php
index bbf2a2a3d86..cc3319da597 100644
--- a/module/VuFind/src/VuFind/Controller/AjaxController.php
+++ b/module/VuFind/src/VuFind/Controller/AjaxController.php
@@ -1259,11 +1259,15 @@ class AjaxController extends AbstractBase
             $base = false;
         }
 
+        $moreOptionsLink = $resolver->supportsMoreOptionsLink()
+            ? $resolver->getResolverUrl($openUrl) : '';
+
         // Render the links using the view:
         $view = [
             'openUrlBase' => $base, 'openUrl' => $openUrl, 'print' => $print,
             'electronic' => $electronic, 'services' => $services,
-            'searchClassId' => $searchClassId
+            'searchClassId' => $searchClassId,
+            'moreOptionsLink' => $moreOptionsLink
         ];
         $html = $this->getViewRenderer()->render('ajax/resolverLinks.phtml', $view);
 
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/AbstractBase.php b/module/VuFind/src/VuFind/Resolver/Driver/AbstractBase.php
new file mode 100644
index 00000000000..2d9f1d5df71
--- /dev/null
+++ b/module/VuFind/src/VuFind/Resolver/Driver/AbstractBase.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * AbstractBase for Resolver Driver
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2015.
+ *
+ * last update: 2011-04-13
+ *
+ * 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  Resolver_Drivers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
+ */
+namespace VuFind\Resolver\Driver;
+
+/**
+ * AbstractBase for Resolver Driver
+ *
+ * @category VuFind
+ * @package  Resolver_Drivers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
+ */
+abstract class AbstractBase implements DriverInterface
+{
+    /**
+     * Base URL for link resolver
+     *
+     * @var string
+     */
+    protected $baseUrl;
+
+    /**
+     * Constructor
+     *
+     * @param string $baseUrl Base URL for link resolver
+     */
+    public function __construct($baseUrl)
+    {
+        $this->baseUrl = $baseUrl;
+    }
+
+    /**
+     * Get Resolver Url
+     *
+     * Transform the OpenURL as needed to get a working link to the resolver.
+     *
+     * @param string $openURL openURL (url-encoded)
+     *
+     * @return string Returns resolver specific url
+     */
+    public function getResolverUrl($openURL)
+    {
+        return $this->baseUrl . '?' . $openURL;
+    }
+
+    /**
+     * This controls whether a "More options" link will be shown below the fetched
+     * resolver links eventually linking to the resolver page previously being
+     * parsed.
+     * This is especially useful for resolver such as the EZB resolver returning
+     * XML which would not be of any immediate use for the user.
+     *
+     * @return bool
+     */
+    public function supportsMoreOptionsLink()
+    {
+        return true;
+    }
+}
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Demo.php b/module/VuFind/src/VuFind/Resolver/Driver/Demo.php
index 43ce20ba428..d864b7cf74d 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Demo.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Demo.php
@@ -39,8 +39,18 @@ use DOMDocument, DOMXpath;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
  */
-class Demo implements DriverInterface
+class Demo extends AbstractBase
 {
+    /**
+     * Constructor
+     *
+     * @param string $baseUrl Base URL for link resolver
+     */
+    public function __construct($baseUrl = 'http://localhost')
+    {
+        parent::__construct($baseUrl);
+    }
+
     /**
      * Fetch Links
      *
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php b/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php
index 7f243ec690d..f0ec93d8310 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php
@@ -66,4 +66,27 @@ interface DriverInterface
      * @return array         Array of values
      */
     public function parseLinks($xmlstr);
+
+    /**
+     * Get Resolver Url
+     *
+     * Transform the OpenURL as needed to get a working link to the resolver.
+     *
+     * @param string $openURL openURL (url-encoded)
+     *
+     * @return string Returns resolver specific url
+     */
+    public function getResolverUrl($openURL);
+
+    /**
+     * This controls whether a "More options" link will be shown below the fetched
+     * resolver links eventually linking to the resolver page previously being
+     * parsed.
+     * This is especially useful for resolver such as the EZB resolver returning
+     * XML which would not be of any immediate use for the user.
+     *
+     * @return bool
+     */
+    public function supportsMoreOptionsLink();
+
 }
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php b/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php
index ad375be428e..c507608d975 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php
@@ -5,6 +5,9 @@
  * EZB is a free service -- the API endpoint is available at
  * http://services.dnb.de/fize-service/gvr/full.xml
  *
+ * API documentation is available at
+ * http://www.zeitschriftendatenbank.de/services/schnittstellen/journals-online-print
+ *
  * PHP version 5
  *
  * Copyright (C) Markus Fischer, info@flyingfischer.ch
@@ -27,6 +30,7 @@
  * @category VuFind
  * @package  Resolver_Drivers
  * @author   Markus Fischer <info@flyingfischer.ch>
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
  */
@@ -39,18 +43,12 @@ use DOMDocument, DOMXpath;
  * @category VuFind
  * @package  Resolver_Drivers
  * @author   Markus Fischer <info@flyingfischer.ch>
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
  */
-class Ezb implements DriverInterface
+class Ezb extends AbstractBase
 {
-    /**
-     * Base URL for link resolver
-     *
-     * @var string
-     */
-    protected $baseUrl;
-
     /**
      * HTTP client
      *
@@ -66,7 +64,7 @@ class Ezb implements DriverInterface
      */
     public function __construct($baseUrl, \Zend\Http\Client $httpClient)
     {
-        $this->baseUrl = $baseUrl;
+        parent::__construct($baseUrl);
         $this->httpClient = $httpClient;
     }
 
@@ -81,32 +79,10 @@ class Ezb implements DriverInterface
      */
     public function fetchLinks($openURL)
     {
-        // Unfortunately the EZB-API only allows OpenURL V0.1 and
-        // breaks when sending a non expected parameter (like an ISBN).
-        // So we do have to 'downgrade' the OpenURL-String from V1.0 to V0.1
-        // and exclude all parameters that are not compliant with the EZB.
-
-        // Parse OpenURL into associative array:
-        $tmp = explode('&', $openURL);
-        $parsed = [];
-
-        foreach ($tmp as $current) {
-            $tmp2 = explode('=', $current, 2);
-            $parsed[$tmp2[0]] = $tmp2[1];
-        }
-
-        // Downgrade 1.0 to 0.1
-        if ($parsed['ctx_ver'] == 'Z39.88-2004') {
-            $openURL = $this->downgradeOpenUrl($parsed);
-        }
-
-        // make the request IP-based to allow automatic
-        // indication on institution level
-        $openURL .= '&pid=client_ip%3D' . $_SERVER['REMOTE_ADDR'];
+        // Get the actual resolver url for the given openUrl
+        $url = $this->getResolverUrl($openURL);
 
         // Make the call to the EZB and load results
-        $url = $this->baseUrl . '?' . $openURL;
-
         $feed = $this->httpClient->setUri($url)->send()->getBody();
         return $feed;
     }
@@ -146,6 +122,60 @@ class Ezb implements DriverInterface
         return $records;
     }
 
+    /**
+     * Get Resolver Url
+     *
+     * Transform the OpenURL as needed to get a working link to the resolver.
+     *
+     * @param string $openURL openURL (url-encoded)
+     *
+     * @return string Link
+     */
+    public function getResolverUrl($openURL)
+    {
+        // Unfortunately the EZB-API only allows OpenURL V0.1 and
+        // breaks when sending a non expected parameter (like an ISBN).
+        // So we do have to 'downgrade' the OpenURL-String from V1.0 to V0.1
+        // and exclude all parameters that are not compliant with the EZB.
+
+        // Parse OpenURL into associative array:
+        $tmp = explode('&', $openURL);
+        $parsed = [];
+
+        foreach ($tmp as $current) {
+            $tmp2 = explode('=', $current, 2);
+            $parsed[$tmp2[0]] = $tmp2[1];
+        }
+
+        // Downgrade 1.0 to 0.1
+        if ($parsed['ctx_ver'] == 'Z39.88-2004') {
+            $openURL = $this->downgradeOpenUrl($parsed);
+        }
+
+        // make the request IP-based to allow automatic
+        // indication on institution level
+        $openURL .= '&pid=client_ip%3D' . $_SERVER['REMOTE_ADDR'];
+
+        // Make the call to the EZB and load results
+        $url = $this->baseUrl . '?' . $openURL;
+
+        return $url;
+    }
+
+    /**
+     * Allows for resolver driver specific enabling/disabling of the more options
+     * link which will link directly to the resolver URL. This should return false if
+     * the resolver returns data in XML or any other human unfriendly response.
+     *
+     * @return bool
+     */
+    public function supportsMoreOptionsLink()
+    {
+        // the EZB link resolver returns unstyled XML which is not helpful for the
+        // user
+        return false;
+    }
+
     /**
      * Downgrade an OpenURL from v1.0 to v0.1 for compatibility with EZB.
      *
@@ -208,25 +238,73 @@ class Ezb implements DriverInterface
             "/OpenURLResponseXML/Full/ElectronicData/ResultList/Result[@state=" .
             $state . "]"
         );
+
+        /*
+         * possible state values:
+         * -1 ISSN nicht eindeutig
+         *  0 Standort-unabhängig frei zugänglich
+         *  1 Standort-unabhängig teilweise zugänglich (Unschärfe bedingt durch
+         *    unspezifische Anfrage oder Moving-Wall)
+         *  2 Lizenziert
+         *  3 Für gegebene Bibliothek teilweise lizenziert (Unschärfe bedingt durch
+         *    unspezifische Anfrage oder Moving-Wall)
+         *  4 nicht lizenziert
+         *  5 Zeitschrift gefunden
+         *    Angaben über Erscheinungsjahr, Datum ... liegen außerhalb des
+         *    hinterlegten bibliothekarischen Zeitraums
+         * 10 Unbekannt (ISSN unbekannt, Bibliothek unbekannt)
+         */
+        $state_access_mapping = [
+            '-1' => 'error',
+            '0'  => 'open',
+            '1'  => 'limited',
+            '2'  => 'open',
+            '3'  => 'limited',
+            '4'  => 'denied',
+            '5'  => 'denied',
+            '10' => 'unknown'
+        ];
+
         $i = 0;
         foreach ($results as $result) {
             $record = [];
             $titleXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/" .
-                "Result[@state={$state}]/Title";
-            $record['title'] = strip_tags(
-                $xpath->query($titleXP, $result)->item($i)->nodeValue
-            );
-            $record['coverage'] = $coverage;
+                "Result[@state={$state}][" . ($i + 1) . "]/Title";
+            $title = $xpath->query($titleXP, $result)->item(0);
+            if (isset($title)) {
+                $record['title'] = strip_tags($title->nodeValue);
+            }
+
+            $additionalXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/" .
+                "Result[@state={$state}][" . ($i + 1) . "]/Additionals/Additional";
+            $additionalType = ['nali', 'intervall', 'moving_wall'];
+            $additionals = [];
+            foreach ($additionalType as $type) {
+                $additional = $xpath
+                    ->query($additionalXP . "[@type='" . $type . "']", $result)
+                    ->item(0);
+                if (isset($additional->nodeValue)) {
+                    $additionals[$type] = strip_tags($additional->nodeValue);
+                }
+            }
+            $record['coverage']
+                = !empty($additionals) ? implode("; ", $additionals) : $coverage;
+
+            $record['access'] = $state_access_mapping[$state];
+
             $urlXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/" .
-                "Result[@state={$state}]/AccessURL";
-            $record['href'] = $xpath->query($urlXP, $result)->item($i)->nodeValue;
+                "Result[@state={$state}][" . ($i + 1) . "]/AccessURL";
+            $url = $xpath->query($urlXP, $result)->item(0);
+            if (isset($url->nodeValue)) {
+                $record['href'] = $url->nodeValue;
+            }
             // Service type needs to be hard-coded for calling code to properly
             // categorize links. The commented code below picks a more appropriate
             // value but won't work for now -- retained for future reference.
             //$service_typeXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/"
-            //    . "Result[@state={$state}]/AccessLevel";
+            //    . "Result[@state={$state}][".($i+1)."]/AccessLevel";
             //$record['service_type']
-            //    = $xpath->query($service_typeXP, $result)->item($i)->nodeValue;
+            //    = $xpath->query($service_typeXP, $result)->item(0)->nodeValue;
             $record['service_type'] = 'getFullTxt';
             array_push($records, $record);
             $i++;
@@ -249,12 +327,51 @@ class Ezb implements DriverInterface
         $results = $xpath->query(
             "/OpenURLResponseXML/Full/PrintData/ResultList/Result[@state={$state}]"
         );
+
+        /*
+         * possible state values:
+         * -1 ISSN nicht eindeutig
+         *  2 Vorhanden
+         *  3 Teilweise vorhanden (Unschärfe bedingt durch unspezifische Anfrage bei
+         *    nicht vollständig vorhandener Zeitschrift)
+         *  4 Nicht vorhanden
+         * 10 Unbekannt (ZDB-ID unbekannt, ISSN unbekannt, Bibliothek unbekannt)
+         */
+        $state_access_mapping = [
+            '-1' => 'error',
+            '2'  => 'open',
+            '3'  => 'limited',
+            '4'  => 'denied',
+            '10' => 'unknown'
+        ];
+
         $i = 0;
         foreach ($results as $result) {
             $record = [];
             $record['title'] = $coverage;
+
+            $resultXP = "/OpenURLResponseXML/Full/PrintData/ResultList/" .
+                "Result[@state={$state}][" . ($i + 1) . "]";
+            $resultElements = [
+                'Title', 'Location', 'Signature', 'Period', 'Holding_comment'
+            ];
+            $elements = [];
+            foreach ($resultElements as $element) {
+                $elem = $xpath->query($resultXP . "/" . $element, $result)->item(0);
+                if (isset($elem->nodeValue)) {
+                    $elements[$element] = strip_tags($elem->nodeValue);
+                }
+            }
+            $record['coverage']
+                = !empty($elements) ? implode("; ", $elements) : $coverage;
+
+            $record['access'] = $state_access_mapping[$state];
+
             $urlXP = "/OpenURLResponseXML/Full/PrintData/References/Reference/URL";
-            $record['href'] = $xpath->query($urlXP, $result)->item($i)->nodeValue;
+            $url = $xpath->query($urlXP, $result)->item($i);
+            if (isset($url->nodeValue)) {
+                $record['href'] = $url->nodeValue;
+            }
             // Service type needs to be hard-coded for calling code to properly
             // categorize links. The commented code below picks a more appropriate
             // value but won't work for now -- retained for future reference.
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Redi.php b/module/VuFind/src/VuFind/Resolver/Driver/Redi.php
index 7d90616ac5e..163fdeef2df 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Redi.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Redi.php
@@ -40,7 +40,7 @@ use DOMDocument, Zend\Dom\DOMXPath;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
  */
-class Redi implements DriverInterface
+class Redi extends AbstractBase
 {
     /**
      * HTTP client
@@ -49,13 +49,6 @@ class Redi implements DriverInterface
      */
     protected $httpClient;
 
-    /**
-     * Base URL for link resolver
-     *
-     * @var string
-     */
-    protected $baseUrl;
-
     /**
      * Parsed resolver links
      *
@@ -71,7 +64,7 @@ class Redi implements DriverInterface
      */
     public function __construct($baseUrl, \Zend\Http\Client $httpClient)
     {
-        $this->baseUrl = $baseUrl;
+        parent::__construct($baseUrl);
         $this->httpClient = $httpClient;
     }
 
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php b/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php
index d2063132907..ccd6a7ec3c6 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php
@@ -39,15 +39,8 @@ namespace VuFind\Resolver\Driver;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
  */
-class Sfx implements DriverInterface
+class Sfx extends AbstractBase
 {
-    /**
-     * Base URL for link resolver
-     *
-     * @var string
-     */
-    protected $baseUrl;
-
     /**
      * HTTP client
      *
@@ -63,7 +56,7 @@ class Sfx implements DriverInterface
      */
     public function __construct($baseUrl, \Zend\Http\Client $httpClient)
     {
-        $this->baseUrl = $baseUrl;
+        parent::__construct($baseUrl);
         $this->httpClient = $httpClient;
     }
 
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php b/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php
index 791dd6ba5e8..0c72b3444f0 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php
@@ -39,15 +39,8 @@ use DOMDocument, DOMXpath;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki
  */
-class Threesixtylink implements DriverInterface
+class Threesixtylink extends AbstractBase
 {
-    /**
-     * Base URL for link resolver
-     *
-     * @var string
-     */
-    protected $baseUrl;
-
     /**
      * HTTP client
      *
@@ -63,7 +56,7 @@ class Threesixtylink implements DriverInterface
      */
     public function __construct($baseUrl, \Zend\Http\Client $httpClient)
     {
-        $this->baseUrl = $baseUrl;
+        parent::__construct($baseUrl);
         $this->httpClient = $httpClient;
     }
 
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Factory.php b/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
index 9544456c0bd..4987ebb9ace 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
@@ -339,9 +339,12 @@ class Factory
             ),
             true
         );
+        $resolverPluginManager = $sm->getServiceLocator()
+            ->get('VuFind\ResolverDriverPluginManager');
         return new OpenUrl(
             $sm->get('context'),
             $openUrlRules,
+            $resolverPluginManager,
             isset($config->OpenURL) ? $config->OpenURL : null
         );
     }
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php b/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php
index 6f1393deedc..e7090ce5037 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php
@@ -26,6 +26,7 @@
  * @link     https://vufind.org/wiki/development Wiki
  */
 namespace VuFind\View\Helper\Root;
+use VuFind\Resolver\Driver\PluginManager;
 
 /**
  * OpenUrl view helper
@@ -59,6 +60,13 @@ class OpenUrl extends \Zend\View\Helper\AbstractHelper
      */
     protected $openUrlRules;
 
+    /**
+     * Resolver plugin manager
+     *
+     * @var PluginManager
+     */
+    protected $resolverPluginManager;
+
     /**
      * Current RecordDriver
      *
@@ -76,15 +84,17 @@ class OpenUrl extends \Zend\View\Helper\AbstractHelper
     /**
      * Constructor
      *
-     * @param \VuFind\View\Helper\Root\Context $context      Context helper
-     * @param array                            $openUrlRules VuFind OpenURL rules
-     * @param \Zend\Config\Config              $config       VuFind OpenURL config
+     * @param \VuFind\View\Helper\Root\Context $context       Context helper
+     * @param array                            $openUrlRules  VuFind OpenURL rules
+     * @param PluginManager                    $pluginManager Resolver plugin manager
+     * @param \Zend\Config\Config              $config        VuFind OpenURL config
      */
     public function __construct(\VuFind\View\Helper\Root\Context $context,
-        $openUrlRules, $config = null
+        $openUrlRules, PluginManager $pluginManager, $config = null
     ) {
         $this->context = $context;
         $this->openUrlRules = $openUrlRules;
+        $this->resolverPluginManager = $pluginManager;
         $this->config = $config;
     }
 
@@ -192,8 +202,20 @@ class OpenUrl extends \Zend\View\Helper\AbstractHelper
             );
         }
 
+        // instantiate the resolver plugin to get a proper resolver link
+        if ($this->resolverPluginManager->has($resolver)) {
+            $resolverObj = new \VuFind\Resolver\Connection(
+                $this->resolverPluginManager->get($resolver)
+            );
+            $resolverUrl = $resolverObj->getResolverUrl($openurl);
+        } else {
+            $resolverUrl = empty($base) ? '' : $base . '?' .
+                $this->recordDriver->getOpenUrl();
+        }
+
         // Build parameters needed to display the control:
         $params = [
+            'resolverUrl' => $resolverUrl,
             'openUrl' => $this->recordDriver->getOpenUrl(),
             'openUrlBase' => empty($base) ? false : $base,
             'openUrlWindow' => empty($this->config->window_settings)
@@ -291,6 +313,11 @@ class OpenUrl extends \Zend\View\Helper\AbstractHelper
      */
     protected function checkIfRulesApply()
     {
+        // special case if no rules are defined at all assume that any record is
+        // valid for openUrls
+        if (!isset($this->openUrlRules) || count($this->openUrlRules) < 1) {
+            return true;
+        }
         foreach ($this->openUrlRules as $rules) {
             if (!$this->checkExcludedRecordsRules($rules)
                 && $this->checkSupportedRecordsRules($rules)
diff --git a/module/VuFind/tests/fixtures/resolver/response/ezb.xml b/module/VuFind/tests/fixtures/resolver/response/ezb.xml
new file mode 100644
index 00000000000..9604c86044f
--- /dev/null
+++ b/module/VuFind/tests/fixtures/resolver/response/ezb.xml
@@ -0,0 +1,110 @@
+HTTP/1.1 200 OK
+Date: Thu, 21 Apr 2016 13:25:49 GMT
+Server: Apache
+Content-Length: 5592
+Vary: Accept-Encoding
+Content-Type: text/xml;charset=UTF-8
+
+
+<OpenURLResponseXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0.0" xsi:noNamespaceSchemaLocation="http://ezb.uni-regensburg.de/ezeit/vascoda/vifa/vifaxml/response_vr_v1_0_1.xsd">
+    <Full>
+        <ElectronicData>
+            <Library>Universitätsbibliothek Leipzig</Library>
+            <References>
+                <Reference>
+                    <URL>http://www.bibliothek.uni-regensburg.de/ezeit/?1482826</URL>
+                    <Label>EZB-Opac</Label>
+                </Reference>
+                <Reference>
+                    <URL>http://www.bibliothek.uni-regensburg.de/ezeit/?2010839</URL>
+                    <Label>EZB-Opac</Label>
+                </Reference>
+                <Reference>
+                    <URL>http://www.bibliothek.uni-regensburg.de/ezeit/search.phtml?bibid=UBL&amp;lang=de&amp;jq_type1=KT&amp;jq_term1=Noûs : a Quarterly Journal of Philosophy (1997-)</URL>
+                    <Label>EZB-Suche</Label>
+                </Reference>
+            </References>
+            <ResultList>
+                <Result state="3">
+                    <Title>Noûs : a Quarterly Journal of Philosophy (1997-)</Title>
+                    <JournalURL>http://onlinelibrary.wiley.com/journal/10.1111/(ISSN)1468-0068</JournalURL>
+                    <AccessURL>http://onlinelibrary.wiley.com/journal/10.1111/(ISSN)1468-0068</AccessURL>
+                    <AccessLevel>homepage</AccessLevel>
+                    <Additionals>
+                        <Additional type="intervall">ab Vol. 31, Iss. 1 (1997)</Additional>
+                    </Additionals>
+                    <ReadmeURL lang="de">https://www.ub.uni-leipzig.de/index.php?id=163#Wiley Core 2015</ReadmeURL>
+                    <ReadmeURL lang="en">http://www.ub.uni-leipzig.de/kata_db/Readme.htm#Wiley Core 2015</ReadmeURL>
+                </Result>
+                <Result state="3">
+                    <Title>Noûs (ältere Jahrgänge via JSTOR)</Title>
+                    <JournalURL>http://www.jstor.org/action/showPublication?journalCode=nous</JournalURL>
+                    <AccessURL>http://www.jstor.org/action/showPublication?journalCode=nous</AccessURL>
+                    <AccessLevel>homepage</AccessLevel>
+                    <Additionals>
+                        <Additional type="intervall">ab Vol. 1, Iss. 1 (1967)</Additional>
+                        <Additional type="moving_wall">für die Ausgaben der aktuellen 11 Jahrgänge nicht verfügbar</Additional>
+                    </Additionals>
+                    <ReadmeURL lang="de">https://www.ub.uni-leipzig.de/index.php?id=163#jstor_arts1</ReadmeURL>
+                    <ReadmeURL lang="en">http://www.ub.uni-leipzig.de/kata_db/Readme.htm#jstor_arts1</ReadmeURL>
+                </Result>
+                <Result state="3">
+                    <Title>Nous (via EBSCO Host)</Title>
+                    <JournalURL>http://search.ebscohost.com/direct.asp?db=aph&amp;jid=D97&amp;scope=site</JournalURL>
+                    <AccessURL>http://search.ebscohost.com/direct.asp?db=aph&amp;jid=D97&amp;scope=site</AccessURL>
+                    <AccessLevel>homepage</AccessLevel>
+                    <Additionals>
+                        <Additional type="moving_wall">für die Ausgaben der vergangenen 12 Monate nicht verfügbar</Additional>
+                    </Additionals>
+                    <ReadmeURL lang="de">https://www.ub.uni-leipzig.de/index.php?id=163#ebsco_aph</ReadmeURL>
+                    <ReadmeURL lang="en">http://www.ub.uni-leipzig.de/kata_db/Readme.htm#ebsco_aph</ReadmeURL>
+                </Result>
+                <Result state="3">
+                    <Title>Nous (via EBSCO Host)</Title>
+                    <JournalURL>http://search.ebscohost.com/direct.asp?db=lfh&amp;jid=D97&amp;scope=site</JournalURL>
+                    <AccessURL>http://search.ebscohost.com/direct.asp?db=lfh&amp;jid=D97&amp;scope=site</AccessURL>
+                    <AccessLevel>homepage</AccessLevel>
+                    <Additionals>
+                        <Additional type="moving_wall">für die Ausgaben der vergangenen 12 Monate nicht verfügbar</Additional>
+                    </Additionals>
+                    <ReadmeURL lang="de">https://www.ub.uni-leipzig.de/index.php?id=163#ebsco_lfh</ReadmeURL>
+                    <ReadmeURL lang="en">http://www.ub.uni-leipzig.de/kata_db/Readme.htm#ebsco_lfh</ReadmeURL>
+                </Result>
+                <Result state="3">
+                    <Title>Philosophical Perspectives (aktuelle Jahrgänge)</Title>
+                    <JournalURL>http://onlinelibrary.wiley.com/journal/10.1111/%28ISSN%291520-8583</JournalURL>
+                    <AccessURL>http://onlinelibrary.wiley.com/journal/10.1111/%28ISSN%291520-8583</AccessURL>
+                    <AccessLevel>homepage</AccessLevel>
+                    <Additionals>
+                        <Additional type="intervall">ab Vol. 17 (2003)</Additional>
+                    </Additionals>
+                    <ReadmeURL lang="de">https://www.ub.uni-leipzig.de/index.php?id=163#Wiley Core 2015</ReadmeURL>
+                    <ReadmeURL lang="en">http://www.ub.uni-leipzig.de/kata_db/Readme.htm#Wiley Core 2015</ReadmeURL>
+                </Result>
+            </ResultList>
+        </ElectronicData>
+        <PrintData>
+            <Library>Universitätsbibliothek Leipzig</Library>
+            <References>
+                <Reference>
+                    <URL>http://dispatch.opac.dnb.de/CHARSET=ISO-8859-1/DB=1.1/CMD?ACT=SRCHA&amp;IKT=8509&amp;SRT=LST_ty&amp;TRM=IDN+011960027+or+IDN+01545794X&amp;HLIB=009030085#009030085</URL>
+                    <Label>ZDB-OPAC</Label>
+                </Reference>
+            </References>
+            <ResultList>
+                <Result state="2">
+                    <Title>Philosophical perspectives</Title>
+                    <Location>Leipzig UB</Location>
+                    <Holding_comment>Nachweis als Serie</Holding_comment>
+                </Result>
+                <Result state="2">
+                    <Title>Noûs</Title>
+                    <Location>Leipzig UB // HB/FH/ Standortsignatur: 96-7-558</Location>
+                    <Signature>CA 5470 Magazin: 96-7-558</Signature>
+                    <Period>1.1967 - 27.1993; 30.1996 - 43.2009</Period>
+                    <Holding_comment>Letzten 15 Jg. Freihand</Holding_comment>
+                </Result>
+            </ResultList>
+        </PrintData>
+    </Full>
+</OpenURLResponseXML>
\ No newline at end of file
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php
new file mode 100644
index 00000000000..11d0dd2b88b
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/EzbTest.php
@@ -0,0 +1,168 @@
+<?php
+/**
+ * Ezb resolver driver test
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Leipzig University Library 2015.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+namespace VuFindTest\Resolver\Driver;
+use VuFind\Resolver\Driver\Ezb;
+
+use Zend\Http\Client\Adapter\Test as TestAdapter;
+use Zend\Http\Response as HttpResponse;
+
+use InvalidArgumentException;
+
+/**
+ * Ezb resolver driver test
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+class EzbTest extends \VuFindTest\Unit\TestCase
+{
+    /**
+     * Test-Config
+     *
+     * @var array
+     */
+    protected $openUrlConfig = [
+        'OpenURL' => [
+            'url' => "http://services.d-nb.de/fize-service/gvr/full.xml",
+            'rfr_id' => "www.ub.uni-leipzig.de",
+            'resolver' => "ezb",
+            'window_settings' => "toolbar=no,location=no,directories=no,buttons=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=550,height=600",
+            'show_in_results' => false,
+            'show_in_record' => false,
+            'show_in_holdings' => true,
+            'embed' => true,
+            'replace_other_urls' => true
+        ],
+    ];
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testParseLinks()
+    {
+        $conn = $this->createConnector('ezb.xml');
+
+        $openUrl = "url_ver=Z39.88-2004&ctx_ver=Z39.88-2004&ctx_enc=info%3Aofi%2Fenc%3AUTF-8&rfr_id=info%3Asid%2Fwww.ub.uni-leipzig.de%3Agenerator&rft.title=No%C3%BBs&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Adc&rft.creator=&rft.pub=Wiley-Blackwell&rft.format=Journal&rft.language=English&rft.issn=0029-4624&zdbid=339287-9";
+        $result = $conn->parseLinks($conn->fetchLinks($openUrl));
+
+        $testResult = [
+            0 => [
+                'title' => 'Noûs : a Quarterly Journal of Philosophy (1997-)',
+                'coverage' => 'ab Vol. 31, Iss. 1 (1997)',
+                'access' => 'limited',
+                'href' => 'http://onlinelibrary.wiley.com/journal/10.1111/(ISSN)1468-0068',
+                'service_type' => 'getFullTxt'
+            ],
+            1 => [
+                'title' => 'Noûs (ältere Jahrgänge via JSTOR)',
+                'coverage' => 'ab Vol. 1, Iss. 1 (1967); für die Ausgaben der aktuellen 11 Jahrgänge nicht verfügbar',
+                'access' => 'limited',
+                'href' => 'http://www.jstor.org/action/showPublication?journalCode=nous',
+                'service_type' => 'getFullTxt'
+            ],
+            2 => [
+                'title' => 'Nous (via EBSCO Host)',
+                'coverage' => 'für die Ausgaben der vergangenen 12 Monate nicht verfügbar',
+                'access' => 'limited',
+                'href' => 'http://search.ebscohost.com/direct.asp?db=aph&jid=D97&scope=site',
+                'service_type' => 'getFullTxt'
+            ],
+            3 => [
+                'title' => 'Nous (via EBSCO Host)',
+                'coverage' => 'für die Ausgaben der vergangenen 12 Monate nicht verfügbar',
+                'access' => 'limited',
+                'href' => 'http://search.ebscohost.com/direct.asp?db=lfh&jid=D97&scope=site',
+                'service_type' => 'getFullTxt'
+            ],
+            4 => [
+                'title' => 'Philosophical Perspectives (aktuelle Jahrgänge)',
+                'coverage' => 'ab Vol. 17 (2003)',
+                'access' => 'limited',
+                'href' => 'http://onlinelibrary.wiley.com/journal/10.1111/%28ISSN%291520-8583',
+                'service_type' => 'getFullTxt'
+            ],
+            5 => [
+                'title' => 'Print available',
+                'coverage' => 'Philosophical perspectives; Leipzig UB; Nachweis als Serie',
+                'access' => 'open',
+                'href' => 'http://dispatch.opac.dnb.de/CHARSET=ISO-8859-1/DB=1.1/CMD?ACT=SRCHA&IKT=8509&SRT=LST_ty&TRM=IDN+011960027+or+IDN+01545794X&HLIB=009030085#009030085',
+                'service_type' => 'getHolding'
+            ],
+            6 => [
+                'title' => 'Print available',
+                'coverage' => 'Noûs; Leipzig UB // HB/FH/ Standortsignatur: 96-7-558; CA 5470 Magazin: 96-7-558; 1.1967 - 27.1993; 30.1996 - 43.2009; Letzten 15 Jg. Freihand',
+                'access' => 'open',
+                'service_type' => 'getHolding'
+            ]
+        ];
+
+        $this->assertEquals($result, $testResult);
+    }
+
+    /**
+     * Create connector with fixture file.
+     *
+     * @param string $fixture Fixture file
+     *
+     * @return Connector
+     *
+     * @throws InvalidArgumentException Fixture file does not exist
+     */
+    protected function createConnector($fixture = null)
+    {
+        $adapter = new TestAdapter();
+        if ($fixture) {
+            $file = realpath(
+                __DIR__ .
+                '/../../../../../../tests/fixtures/resolver/response/' . $fixture
+            );
+            if (!is_string($file) || !file_exists($file) || !is_readable($file)) {
+                throw new InvalidArgumentException(
+                    sprintf('Unable to load fixture file: %s ', $file)
+                );
+            }
+            $response = file_get_contents($file);
+            $responseObj = HttpResponse::fromString($response);
+            $adapter->setResponse($responseObj);
+        }
+        $_SERVER['REMOTE_ADDR'] = "127.0.0.1";
+
+        $client = new \Zend\Http\Client();
+        $client->setAdapter($adapter);
+
+        $conn = new Ezb($this->openUrlConfig['OpenURL']['url'], $client);
+        return $conn;
+    }
+}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php
index 6cd82aec1ce..5c02af1a1c1 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php
@@ -295,7 +295,9 @@ class OpenUrlTest extends \VuFindTest\Unit\ViewHelperTestCase
         if (null === $mockContext) {
             $mockContext = $this->getMockContext();
         }
-        $openUrl = new OpenUrl($mockContext, $rules, new Config($config));
+        $mockPm = $this->getMockBuilder('VuFind\Resolver\Driver\PluginManager')
+            ->getMock();
+        $openUrl = new OpenUrl($mockContext, $rules, $mockPm, new Config($config));
         $openUrl->setView($this->getPhpRenderer());
         return $openUrl;
     }
diff --git a/themes/bootstrap3/templates/Helpers/openurl.phtml b/themes/bootstrap3/templates/Helpers/openurl.phtml
index f68528b4b61..22d5cac3e2f 100644
--- a/themes/bootstrap3/templates/Helpers/openurl.phtml
+++ b/themes/bootstrap3/templates/Helpers/openurl.phtml
@@ -16,7 +16,7 @@
 
 <span class="openUrlControls<? if ($this->openUrlEmbed): ?> openUrlEmbed<? if ($this->openUrlEmbedAutoLoad): ?> openUrlEmbedAutoLoad<? endif; ?><? endif; ?>">
   <? if (!$this->openUrlImageBasedSrc || $this->openUrlImageBasedMode == 'both'): ?>
-  <a href="<?=$this->escapeHtmlAttr($this->openUrlBase . '?' . $this->openUrl)?>"<?=$class?> data-search-class-id="<?=$this->escapeHtmlAttr($this->searchClassId) ?>">
+  <a href="<?=$this->escapeHtmlAttr($this->resolverUrl)?>"<?=$class?> data-search-class-id="<?=$this->escapeHtmlAttr($this->searchClassId) ?>">
     <? /* put the openUrl here in a span (COinS almost) so we can retrieve it later */ ?>
     <span title="<?=$this->escapeHtmlAttr($this->openUrl)?>" class="openUrl"></span>
     <? if ($this->openUrlGraphic): ?>
diff --git a/themes/bootstrap3/templates/ajax/resolverLinks.phtml b/themes/bootstrap3/templates/ajax/resolverLinks.phtml
index 735b8f2b51a..2a602e55378 100644
--- a/themes/bootstrap3/templates/ajax/resolverLinks.phtml
+++ b/themes/bootstrap3/templates/ajax/resolverLinks.phtml
@@ -32,7 +32,7 @@
     </div>
   <? endif; ?>
   <div class="openurls">
-    <strong><a href="<?=$this->escapeHtmlAttr($this->openUrlBase)?>?<?=$this->escapeHtmlAttr($this->openUrl)?>"><?=$this->transEsc('More options')?></a></strong>
+    <? if (!empty($this->moreOptionsLink)): ?><strong><a href="<?=$this->escapeHtmlAttr($this->moreOptionsLink)?>"><?=$this->transEsc('More options')?></a></strong><?endif; ?>
     <? if (!empty($this->services)): ?>
       <ul>
         <? foreach ($this->services as $link): ?>
-- 
GitLab