diff --git a/module/finc/src/finc/Resolver/Driver/EzbTrait.php b/module/finc/src/finc/Resolver/Driver/EzbTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c74a4a2cec9c6b15d097556dfc093fe843a7515
--- /dev/null
+++ b/module/finc/src/finc/Resolver/Driver/EzbTrait.php
@@ -0,0 +1,254 @@
+<?php
+/**
+ * EZB Link Resolver Driver Trait
+ *
+ * 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/journals-online-print
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Markus Fischer, info@flyingfischer.ch
+ *
+ * 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   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
+ */
+namespace finc\Resolver\Driver;
+
+use DOMDocument;
+use DOMXpath;
+
+/**
+ * EZB Link Resolver Driver
+ *
+ * @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
+ */
+trait EzbTrait
+{
+    /**
+     * As the JOP resolver provides also generic labels 'Article', 'Journal'
+     * etc. in element AccessLevel this label can be used as title for
+     * resolver results by setting this variable to 'AccessLevel'
+     *
+     * @var string
+     */
+    protected static $xpathTitleSelector = 'Title';
+
+    /**
+     * Parse Links
+     *
+     * Parses an XML file returned by a link resolver
+     * and converts it to a standardised format for display
+     *
+     * @param string $xmlstr Raw XML returned by resolver
+     *
+     * @return array         Array of values
+     */
+    public function parseLinks($xmlstr)
+    {
+        $records = []; // array to return
+
+        $xml = new DOMDocument();
+        if (!@$xml->loadXML($xmlstr)) {
+            return $records;
+        }
+
+        $xpath = new DOMXpath($xml);
+
+        // get results for online
+        $this->getElectronicResults('0', 'Free', $records, $xpath);
+        $this->getElectronicResults('1', 'Partially free', $records, $xpath);
+        $this->getElectronicResults('2', 'Licensed', $records, $xpath);
+        $this->getElectronicResults('3', 'Partially licensed', $records, $xpath);
+        $this->getElectronicResults('4', 'Not free', $records, $xpath);
+        $this->getElectronicResults('10', 'Unknown Electronic', $records, $xpath);
+
+        // get results for print, only if available
+        $this->getPrintResults('2', 'Print available', $records, $xpath);
+        $this->getPrintResults('3', 'Print partially available', $records, $xpath);
+        $this->getPrintResults('10', 'Unknown Print', $records, $xpath);
+
+        return $records;
+    }
+
+    /**
+     * Downgrade an OpenURL from v1.0 to v0.1 for compatibility with EZB.
+     *
+     * @param array $parsed Array of parameters parsed from the OpenURL.
+     *
+     * @return string       EZB-compatible v0.1 OpenURL
+     */
+    protected function downgradeOpenUrl($parsed)
+    {
+        $downgraded = [];
+
+        // prepare content for downgrading
+        // resolver only accepts date formats YYYY, YYYY-MM, and YYYY-MM-DD
+        // in case we have a date in another format, drop the date information
+        if (isset($parsed['rft.date'])
+            && !preg_match('/^\d{4}(-\d\d(-\d\d)?)?$/', $parsed['rft.date'])
+        ) {
+            unset($parsed['rft.date']);
+        }
+
+        $map = [
+            'rfr_id' => 'sid',
+            'rft.date' => 'date',
+            'rft.issn' => 'issn',
+            'rft.isbn' => 'isbn', // isbn is supported as of 12/2021
+            'rft.volume' => 'volume',
+            'rft.issue' => 'issue',
+            'rft.spage' => 'spage',
+            'rft.pages' => 'pages',
+        ];
+
+        // ignore all other parameters
+        foreach ($parsed as $key => $value) {
+            // exclude empty parameters
+            if (isset($value) && $value !== '') {
+                if (isset($map[$key])) {
+                    $downgraded[] = "{$map[$key]}=$value";
+                } elseif (in_array($key, $map)) {
+                    $downgraded[] = "$key=$value";
+                }
+            }
+        }
+        if (!empty($downgraded)) {
+            // we need 'genre' but only the values
+            // article or journal are allowed...
+            return "genre=article&" . implode('&', $downgraded);
+        }
+
+        return implode('&', $parsed);
+    }
+
+    /**
+     * Extract electronic results from the EZB response and inject them into the
+     * $records array.
+     *
+     * @param string   $state    The state attribute value to extract
+     * @param string   $coverage The coverage string to associate with the state
+     * @param array    $records  The array of results to update
+     * @param DOMXpath $xpath    The XPath object containing parsed XML
+     *
+     * @return void
+     */
+    protected function getElectronicResults($state, $coverage, &$records, $xpath)
+    {
+        $results = $xpath->query(
+            "/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' => 'open',
+            '2' => 'limited',
+            '3' => 'limited',
+            '4' => 'denied',
+            '5' => 'denied',
+            '10' => 'unknown'
+        ];
+
+        $i = 0;
+        foreach ($results as $result) {
+            $record = [];
+
+            // get title from XPath Element defined in $xpathTitleSelector
+            $titleXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/" .
+                "Result[@state={$state}][" . ($i + 1) . "]/" .
+                static::$xpathTitleSelector;
+            $title = $xpath->query($titleXP, $result)->item(0);
+            if (isset($title)) {
+                $record['title'] = strip_tags($title->nodeValue);
+            }
+
+            // get additional coverage information
+            $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];
+
+            // try to find direct access URL
+            $accessUrlXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/" .
+                "Result[@state={$state}][" . ($i + 1) . "]/AccessURL";
+            $accessUrl = $xpath->query($accessUrlXP, $result)->item(0);
+
+            // try to find journal URL as fallback for direct access URL
+            $journalUrlXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/" .
+                "Result[@state={$state}][" . ($i + 1) . "]/JournalURL";
+            $journalUrl = $xpath->query($journalUrlXP, $result)->item(0);
+
+            // return direct access URL if available otherwise journal URL fallback
+            if (isset($accessUrl->nodeValue)) {
+                $record['href'] = $accessUrl->nodeValue;
+            } elseif (isset($journalUrl)) {
+                $record['href'] = $journalUrl->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}][".($i+1)."]/AccessLevel";
+            //$record['service_type']
+            //    = $xpath->query($service_typeXP, $result)->item(0)->nodeValue;
+            $record['service_type'] = 'getFullTxt';
+            array_push($records, $record);
+            $i++;
+        }
+    }
+}