Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.34% |
100 / 106 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
Jop | |
94.34% |
100 / 106 |
|
50.00% |
4 / 8 |
30.16 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
fetchLinks | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
parseLinks | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
2.00 | |||
getResolverUrl | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
supportsMoreOptionsLink | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
downgradeOpenUrl | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
8.38 | |||
getElectronicResults | |
94.74% |
36 / 38 |
|
0.00% |
0 / 1 |
8.01 | |||
getPrintResults | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * JOP Link Resolver Driver |
5 | * |
6 | * JOP is a free service -- the API endpoint is available at |
7 | * http://services.dnb.de/fize-service/gvr/full.xml |
8 | * |
9 | * API documentation is available at |
10 | * http://www.zeitschriftendatenbank.de/services/journals-online-print |
11 | * |
12 | * PHP version 8 |
13 | * |
14 | * Copyright (C) Markus Fischer, info@flyingfischer.ch |
15 | * |
16 | * last update: 2011-04-13 |
17 | * |
18 | * This program is free software; you can redistribute it and/or modify |
19 | * it under the terms of the GNU General Public License version 2, |
20 | * as published by the Free Software Foundation. |
21 | * |
22 | * This program is distributed in the hope that it will be useful, |
23 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
24 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
25 | * GNU General Public License for more details. |
26 | * |
27 | * You should have received a copy of the GNU General Public License |
28 | * along with this program; if not, write to the Free Software |
29 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
30 | * |
31 | * @category VuFind |
32 | * @package Resolver_Drivers |
33 | * @author Markus Fischer <info@flyingfischer.ch> |
34 | * @author André Lahmann <lahmann@ub.uni-leipzig.de> |
35 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
36 | * @link https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki |
37 | */ |
38 | |
39 | namespace VuFind\Resolver\Driver; |
40 | |
41 | use DOMDocument; |
42 | use DOMXpath; |
43 | use VuFind\Net\UserIpReader; |
44 | |
45 | use function in_array; |
46 | |
47 | /** |
48 | * JOP Link Resolver Driver |
49 | * |
50 | * @category VuFind |
51 | * @package Resolver_Drivers |
52 | * @author Markus Fischer <info@flyingfischer.ch> |
53 | * @author André Lahmann <lahmann@ub.uni-leipzig.de> |
54 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
55 | * @link https://vufind.org/wiki/development:plugins:link_resolver_drivers Wiki |
56 | */ |
57 | class Jop extends AbstractBase |
58 | { |
59 | /** |
60 | * As the JOP resolver provides also generic labels 'Article', 'Journal' |
61 | * etc. in element AccessLevel this label can be used as title for |
62 | * resolver results by setting this variable to 'AccessLevel' |
63 | * |
64 | * @var string |
65 | */ |
66 | protected $xpathTitleSelector = 'Title'; |
67 | |
68 | /** |
69 | * HTTP client |
70 | * |
71 | * @var \Laminas\Http\Client |
72 | */ |
73 | protected $httpClient; |
74 | |
75 | /** |
76 | * User IP address reader |
77 | * |
78 | * @var UserIpReader |
79 | */ |
80 | protected $userIpReader; |
81 | |
82 | /** |
83 | * Constructor |
84 | * |
85 | * @param string $baseUrl Base URL for link resolver |
86 | * @param \Laminas\Http\Client $httpClient HTTP client |
87 | * @param UserIpReader $userIpReader User IP address reader |
88 | */ |
89 | public function __construct( |
90 | $baseUrl, |
91 | \Laminas\Http\Client $httpClient, |
92 | UserIpReader $userIpReader |
93 | ) { |
94 | parent::__construct($baseUrl); |
95 | $this->httpClient = $httpClient; |
96 | $this->userIpReader = $userIpReader; |
97 | } |
98 | |
99 | /** |
100 | * Fetch Links |
101 | * |
102 | * Fetches a set of links corresponding to an OpenURL |
103 | * |
104 | * @param string $openURL openURL (url-encoded) |
105 | * |
106 | * @return string raw XML returned by resolver |
107 | */ |
108 | public function fetchLinks($openURL) |
109 | { |
110 | // Get the actual resolver url for the given openUrl |
111 | $url = $this->getResolverUrl($openURL); |
112 | |
113 | // Make the call to the JOP and load results |
114 | $feed = $this->httpClient->setUri($url)->send()->getBody(); |
115 | return $feed; |
116 | } |
117 | |
118 | /** |
119 | * Parse Links |
120 | * |
121 | * Parses an XML file returned by a link resolver |
122 | * and converts it to a standardised format for display |
123 | * |
124 | * @param string $xmlstr Raw XML returned by resolver |
125 | * |
126 | * @return array Array of values |
127 | */ |
128 | public function parseLinks($xmlstr) |
129 | { |
130 | $records = []; // array to return |
131 | |
132 | $xml = new DOMDocument(); |
133 | if (!@$xml->loadXML($xmlstr)) { |
134 | return $records; |
135 | } |
136 | |
137 | $xpath = new DOMXpath($xml); |
138 | |
139 | // get results for online |
140 | $this->getElectronicResults('0', 'Free', $records, $xpath); |
141 | $this->getElectronicResults('1', 'Partially free', $records, $xpath); |
142 | $this->getElectronicResults('2', 'Licensed', $records, $xpath); |
143 | $this->getElectronicResults('3', 'Partially licensed', $records, $xpath); |
144 | $this->getElectronicResults('4', 'Not free', $records, $xpath); |
145 | $this->getElectronicResults('10', 'Unknown Electronic', $records, $xpath); |
146 | |
147 | // get results for print, only if available |
148 | $this->getPrintResults('2', 'Print available', $records, $xpath); |
149 | $this->getPrintResults('3', 'Print partially available', $records, $xpath); |
150 | $this->getPrintResults('10', 'Unknown Print', $records, $xpath); |
151 | |
152 | return $records; |
153 | } |
154 | |
155 | /** |
156 | * Get Resolver Url |
157 | * |
158 | * Transform the OpenURL as needed to get a working link to the resolver. |
159 | * |
160 | * @param string $openURL openURL (url-encoded) |
161 | * |
162 | * @return string Link |
163 | */ |
164 | public function getResolverUrl($openURL) |
165 | { |
166 | // Unfortunately the JOP-API only allows OpenURL V0.1 and |
167 | // breaks when sending a non expected parameter. |
168 | // So we do have to 'downgrade' the OpenURL-String from V1.0 to V0.1 |
169 | // and exclude all parameters that are not compliant with the JOP. |
170 | |
171 | // Parse OpenURL into associative array: |
172 | $tmp = explode('&', $openURL); |
173 | $parsed = []; |
174 | |
175 | foreach ($tmp as $current) { |
176 | $tmp2 = explode('=', $current, 2); |
177 | $parsed[$tmp2[0]] = $tmp2[1] ?? null; |
178 | } |
179 | |
180 | // Downgrade 1.0 to 0.1 |
181 | if ($parsed['ctx_ver'] ?? null == 'Z39.88-2004') { |
182 | $openURL = $this->downgradeOpenUrl($parsed); |
183 | } |
184 | |
185 | // make the request IP-based to allow automatic |
186 | // indication on institution level |
187 | $ipAddr = $this->userIpReader->getUserIp(); |
188 | $openURL .= '&pid=client_ip%3D' . urlencode($ipAddr); |
189 | |
190 | // Make the call to the JOP and load results |
191 | $url = $this->baseUrl . '?' . $openURL; |
192 | |
193 | return $url; |
194 | } |
195 | |
196 | /** |
197 | * Allows for resolver driver specific enabling/disabling of the more options |
198 | * link which will link directly to the resolver URL. This should return false if |
199 | * the resolver returns data in XML or any other human unfriendly response. |
200 | * |
201 | * @return bool |
202 | */ |
203 | public function supportsMoreOptionsLink() |
204 | { |
205 | // the JOP link resolver returns unstyled XML which is not helpful for the |
206 | // user |
207 | return false; |
208 | } |
209 | |
210 | /** |
211 | * Downgrade an OpenURL from v1.0 to v0.1 for compatibility with JOP. |
212 | * |
213 | * @param array $parsed Array of parameters parsed from the OpenURL. |
214 | * |
215 | * @return string JOP-compatible v0.1 OpenURL |
216 | */ |
217 | protected function downgradeOpenUrl($parsed) |
218 | { |
219 | // we need 'genre' but only the values |
220 | // article or journal are allowed... |
221 | $downgraded[] = 'genre=article'; |
222 | |
223 | // prepare content for downgrading |
224 | // resolver only accepts date formats YYYY, YYYY-MM, and YYYY-MM-DD |
225 | // in case we have a date in another format, drop the date information |
226 | if ( |
227 | isset($parsed['rft.date']) |
228 | && !preg_match('/^\d{4}(-\d\d(-\d\d)?)?$/', $parsed['rft.date']) |
229 | ) { |
230 | unset($parsed['rft.date']); |
231 | } |
232 | |
233 | $map = [ |
234 | 'rfr_id' => 'sid', |
235 | 'rft.date' => 'date', |
236 | 'rft.issn' => 'issn', |
237 | 'rft.isbn' => 'isbn', // isbn is supported as of 12/2021 |
238 | 'rft.volume' => 'volume', |
239 | 'rft.issue' => 'issue', |
240 | 'rft.spage' => 'spage', |
241 | 'rft.pages' => 'pages', |
242 | ]; |
243 | |
244 | // ignore all other parameters |
245 | foreach ($parsed as $key => $value) { |
246 | // exclude empty parameters |
247 | if (isset($value) && $value !== '') { |
248 | if (isset($map[$key])) { |
249 | $downgraded[] = "{$map[$key]}=$value"; |
250 | } elseif (in_array($key, $map)) { |
251 | $downgraded[] = "$key=$value"; |
252 | } |
253 | } |
254 | } |
255 | |
256 | return implode('&', $downgraded); |
257 | } |
258 | |
259 | /** |
260 | * Extract electronic results from the JOP response and inject them into the |
261 | * $records array. |
262 | * |
263 | * @param string $state The state attribute value to extract |
264 | * @param string $coverage The coverage string to associate with the state |
265 | * @param array $records The array of results to update |
266 | * @param DOMXpath $xpath The XPath object containing parsed XML |
267 | * |
268 | * @return void |
269 | */ |
270 | protected function getElectronicResults($state, $coverage, &$records, $xpath) |
271 | { |
272 | $results = $xpath->query( |
273 | '/OpenURLResponseXML/Full/ElectronicData/ResultList/Result[@state=' . |
274 | $state . ']' |
275 | ); |
276 | |
277 | /* |
278 | * possible state values: |
279 | * -1 ISSN nicht eindeutig |
280 | * 0 Standort-unabhängig frei zugänglich |
281 | * 1 Standort-unabhängig teilweise zugänglich (Unschärfe bedingt durch |
282 | * unspezifische Anfrage oder Moving-Wall) |
283 | * 2 Lizenziert |
284 | * 3 Für gegebene Bibliothek teilweise lizenziert (Unschärfe bedingt durch |
285 | * unspezifische Anfrage oder Moving-Wall) |
286 | * 4 nicht lizenziert |
287 | * 5 Zeitschrift gefunden |
288 | * Angaben über Erscheinungsjahr, Datum ... liegen außerhalb des |
289 | * hinterlegten bibliothekarischen Zeitraums |
290 | * 10 Unbekannt (ISSN unbekannt, Bibliothek unbekannt) |
291 | */ |
292 | $state_access_mapping = [ |
293 | '-1' => 'error', |
294 | '0' => 'open', |
295 | '1' => 'open', |
296 | '2' => 'limited', |
297 | '3' => 'limited', |
298 | '4' => 'denied', |
299 | '5' => 'denied', |
300 | '10' => 'unknown', |
301 | ]; |
302 | |
303 | $i = 0; |
304 | foreach ($results as $result) { |
305 | $record = []; |
306 | |
307 | // get title from XPath Element defined in $xpathTitleSelector |
308 | $titleXP = '/OpenURLResponseXML/Full/ElectronicData/ResultList/' . |
309 | "Result[@state={$state}][" . ($i + 1) . ']/' . |
310 | $this->xpathTitleSelector; |
311 | $title = $xpath->query($titleXP, $result)->item(0); |
312 | if (isset($title)) { |
313 | $record['title'] = strip_tags($title->nodeValue); |
314 | } |
315 | |
316 | // get additional coverage information |
317 | $additionalXP = '/OpenURLResponseXML/Full/ElectronicData/ResultList/' . |
318 | "Result[@state={$state}][" . ($i + 1) . ']/Additionals/Additional'; |
319 | $additionalType = ['nali', 'intervall', 'moving_wall']; |
320 | $additionals = []; |
321 | foreach ($additionalType as $type) { |
322 | $additional = $xpath |
323 | ->query($additionalXP . "[@type='" . $type . "']", $result) |
324 | ->item(0); |
325 | if (isset($additional->nodeValue)) { |
326 | $additionals[$type] = strip_tags($additional->nodeValue); |
327 | } |
328 | } |
329 | $record['coverage'] |
330 | = !empty($additionals) ? implode('; ', $additionals) : $coverage; |
331 | |
332 | $record['access'] = $state_access_mapping[$state]; |
333 | |
334 | // try to find direct access URL |
335 | $accessUrlXP = '/OpenURLResponseXML/Full/ElectronicData/ResultList/' . |
336 | "Result[@state={$state}][" . ($i + 1) . ']/AccessURL'; |
337 | $accessUrl = $xpath->query($accessUrlXP, $result)->item(0); |
338 | |
339 | // try to find journal URL as fallback for direct access URL |
340 | $journalUrlXP = '/OpenURLResponseXML/Full/ElectronicData/ResultList/' . |
341 | "Result[@state={$state}][" . ($i + 1) . ']/JournalURL'; |
342 | $journalUrl = $xpath->query($journalUrlXP, $result)->item(0); |
343 | |
344 | // return direct access URL if available otherwise journal URL fallback |
345 | if (isset($accessUrl->nodeValue)) { |
346 | $record['href'] = $accessUrl->nodeValue; |
347 | } elseif (isset($journalUrl)) { |
348 | $record['href'] = $journalUrl->nodeValue; |
349 | } |
350 | // Service type needs to be hard-coded for calling code to properly |
351 | // categorize links. The commented code below picks a more appropriate |
352 | // value but won't work for now -- retained for future reference. |
353 | //$service_typeXP = "/OpenURLResponseXML/Full/ElectronicData/ResultList/" |
354 | // . "Result[@state={$state}][".($i+1)."]/AccessLevel"; |
355 | //$record['service_type'] |
356 | // = $xpath->query($service_typeXP, $result)->item(0)->nodeValue; |
357 | $record['service_type'] = 'getFullTxt'; |
358 | array_push($records, $record); |
359 | $i++; |
360 | } |
361 | } |
362 | |
363 | /** |
364 | * Extract print results from the JOP response and inject them into the |
365 | * $records array. |
366 | * |
367 | * @param string $state The state attribute value to extract |
368 | * @param string $coverage The coverage string to associate with the state |
369 | * @param array $records The array of results to update |
370 | * @param DOMXpath $xpath The XPath object containing parsed XML |
371 | * |
372 | * @return void |
373 | */ |
374 | protected function getPrintResults($state, $coverage, &$records, $xpath) |
375 | { |
376 | $results = $xpath->query( |
377 | "/OpenURLResponseXML/Full/PrintData/ResultList/Result[@state={$state}]" |
378 | ); |
379 | |
380 | /* |
381 | * possible state values: |
382 | * -1 ISSN nicht eindeutig |
383 | * 2 Vorhanden |
384 | * 3 Teilweise vorhanden (Unschärfe bedingt durch unspezifische Anfrage bei |
385 | * nicht vollständig vorhandener Zeitschrift) |
386 | * 4 Nicht vorhanden |
387 | * 10 Unbekannt (ZDB-ID unbekannt, ISSN unbekannt, Bibliothek unbekannt) |
388 | */ |
389 | $state_access_mapping = [ |
390 | '-1' => 'error', |
391 | '2' => 'open', |
392 | '3' => 'limited', |
393 | '4' => 'denied', |
394 | '10' => 'unknown', |
395 | ]; |
396 | |
397 | $i = 0; |
398 | foreach ($results as $result) { |
399 | $record = []; |
400 | $record['title'] = $coverage; |
401 | |
402 | $resultXP = '/OpenURLResponseXML/Full/PrintData/ResultList/' . |
403 | "Result[@state={$state}][" . ($i + 1) . ']'; |
404 | $resultElements = [ |
405 | 'Title', 'Location', 'Signature', 'Period', 'Holding_comment', |
406 | ]; |
407 | $elements = []; |
408 | foreach ($resultElements as $element) { |
409 | $elem = $xpath->query($resultXP . '/' . $element, $result)->item(0); |
410 | if (isset($elem->nodeValue)) { |
411 | $elements[$element] = strip_tags($elem->nodeValue); |
412 | } |
413 | } |
414 | $record['coverage'] |
415 | = !empty($elements) ? implode('; ', $elements) : $coverage; |
416 | |
417 | $record['access'] = $state_access_mapping[$state]; |
418 | |
419 | $urlXP = '/OpenURLResponseXML/Full/PrintData/References/Reference/URL'; |
420 | $url = $xpath->query($urlXP, $result)->item($i); |
421 | if (isset($url->nodeValue)) { |
422 | $record['href'] = $url->nodeValue; |
423 | } |
424 | // Service type needs to be hard-coded for calling code to properly |
425 | // categorize links. The commented code below picks a more appropriate |
426 | // value but won't work for now -- retained for future reference. |
427 | //$service_typeXP = "/OpenURLResponseXML/Full/PrintData/References" |
428 | // . "/Reference/Label"; |
429 | //$record['service_type'] |
430 | // = $xpath->query($service_typeXP, $result)->item($i)->nodeValue; |
431 | $record['service_type'] = 'getHolding'; |
432 | array_push($records, $record); |
433 | $i++; |
434 | } |
435 | } |
436 | } |