Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.14% covered (warning)
65.14%
256 / 393
51.28% covered (warning)
51.28%
20 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
DAIA
65.14% covered (warning)
65.14%
256 / 393
51.28% covered (warning)
51.28%
20 / 39
1601.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
72.22% covered (warning)
72.22%
26 / 36
0.00% covered (danger)
0.00%
0 / 1
13.59
 getCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHoldLink
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getStatus
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
13.12
 getStatuses
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
210
 getHolding
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPurchaseHistory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 convertDate
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 convertDatetime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doHTTPRequest
34.69% covered (danger)
34.69%
17 / 49
0.00% covered (danger)
0.00%
0 / 1
16.03
 generateURI
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 generateMultiURIs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 parseDaiaDoc
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 extractDaiaDoc
54.55% covered (warning)
54.55%
12 / 22
0.00% covered (danger)
0.00%
0 / 1
32.41
 convertDaiaXmlToJson
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
15
 parseDaiaArray
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
7
 getItemStatus
80.23% covered (warning)
80.23%
69 / 86
0.00% covered (danger)
0.00%
0 / 1
31.22
 getCustomData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStatusString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkIsRecallable
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
11
 checkIsStorageRetrievalRequest
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
11
 getHoldType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getItemLimitation
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getItemDepartment
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 getItemDepartmentId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getItemDepartmentLink
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getItemStorage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 getItemStorageId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getItemStorageLink
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getItemLimitationContent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getItemLimitationTypes
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getItemNumber
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getItemBarcode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getItemReserveStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getItemCallnumber
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getAvailableItemServices
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 logMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * ILS Driver for VuFind to query availability information via DAIA.
5 *
6 * Based on the proof-of-concept-driver by Till Kinstler, GBV.
7 * Relaunch of the daia driver developed by Oliver Goldschmidt.
8 *
9 * PHP version 8
10 *
11 * Copyright (C) Jochen Lienhard 2014.
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License version 2,
15 * as published by the Free Software Foundation.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
25 *
26 * @category VuFind
27 * @package  ILS_Drivers
28 * @author   Jochen Lienhard <lienhard@ub.uni-freiburg.de>
29 * @author   Oliver Goldschmidt <o.goldschmidt@tu-harburg.de>
30 * @author   AndrĂ© Lahmann <lahmann@ub.uni-leipzig.de>
31 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
32 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
33 */
34
35namespace VuFind\ILS\Driver;
36
37use DOMDocument;
38use Laminas\Log\LoggerAwareInterface as LoggerAwareInterface;
39use VuFind\Exception\ILS as ILSException;
40use VuFindHttp\HttpServiceAwareInterface as HttpServiceAwareInterface;
41
42use function count;
43use function in_array;
44use function is_array;
45use function strlen;
46
47/**
48 * ILS Driver for VuFind to query availability information via DAIA.
49 *
50 * @category VuFind
51 * @package  ILS_Drivers
52 * @author   Jochen Lienhard <lienhard@ub.uni-freiburg.de>
53 * @author   Oliver Goldschmidt <o.goldschmidt@tu-harburg.de>
54 * @author   AndrĂ© Lahmann <lahmann@ub.uni-leipzig.de>
55 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
56 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
57 */
58class DAIA extends AbstractBase implements
59    HttpServiceAwareInterface,
60    LoggerAwareInterface
61{
62    use \VuFind\Cache\CacheTrait {
63        getCacheKey as protected getBaseCacheKey;
64    }
65    use \VuFindHttp\HttpServiceAwareTrait;
66    use \VuFind\Log\LoggerAwareTrait;
67
68    /**
69     * Base URL for DAIA Service
70     *
71     * @var string
72     */
73    protected $baseUrl;
74
75    /**
76     * Timeout in seconds to be used for DAIA http requests
77     *
78     * @var string
79     */
80    protected $daiaTimeout = null;
81
82    /**
83     * Flag to switch on/off caching for DAIA items
84     *
85     * @var bool
86     */
87    protected $daiaCacheEnabled = false;
88
89    /**
90     * DAIA query identifier prefix
91     *
92     * @var string
93     */
94    protected $daiaIdPrefix;
95
96    /**
97     * DAIA response format
98     *
99     * @var string
100     */
101    protected $daiaResponseFormat;
102
103    /**
104     * Flag to enable multiple DAIA-queries
105     *
106     * @var bool
107     */
108    protected $multiQuery = false;
109
110    /**
111     * Acceptable ContentTypes delivered by DAIA server in HTTP header
112     *
113     * @var array
114     */
115    protected $contentTypesResponse;
116
117    /**
118     * ContentTypes to use in DAIA HTTP requests in HTTP header
119     *
120     * @var array
121     */
122    protected $contentTypesRequest = [
123        'xml'  => 'application/xml',
124        'json' => 'application/json',
125    ];
126
127    /**
128     * Date converter object
129     *
130     * @var \VuFind\Date\Converter
131     */
132    protected $dateConverter;
133
134    /**
135     * Constructor
136     *
137     * @param \VuFind\Date\Converter $converter Date converter
138     */
139    public function __construct(\VuFind\Date\Converter $converter)
140    {
141        $this->dateConverter = $converter;
142    }
143
144    /**
145     * Initialize the driver.
146     *
147     * Validate configuration and perform all resource-intensive tasks needed to
148     * make the driver active.
149     *
150     * @throws ILSException
151     * @return void
152     */
153    public function init()
154    {
155        if (isset($this->config['DAIA']['baseUrl'])) {
156            $this->baseUrl = $this->config['DAIA']['baseUrl'];
157        } elseif (isset($this->config['Global']['baseUrl'])) {
158            throw new ILSException(
159                'Deprecated [Global] section in DAIA.ini present, but no [DAIA] ' .
160                'section found: please update DAIA.ini (cf. config/vufind/DAIA.ini).'
161            );
162        } else {
163            throw new ILSException('DAIA/baseUrl configuration needs to be set.');
164        }
165        // use DAIA specific timeout setting for http requests if configured
166        if ((isset($this->config['DAIA']['timeout']))) {
167            $this->daiaTimeout = $this->config['DAIA']['timeout'];
168        }
169        if (isset($this->config['DAIA']['daiaResponseFormat'])) {
170            $this->daiaResponseFormat = strtolower(
171                $this->config['DAIA']['daiaResponseFormat']
172            );
173        } else {
174            $this->debug('No daiaResponseFormat setting found, using default: xml');
175            $this->daiaResponseFormat = 'xml';
176        }
177        if (isset($this->config['DAIA']['daiaIdPrefix'])) {
178            $this->daiaIdPrefix = $this->config['DAIA']['daiaIdPrefix'];
179        } else {
180            $this->debug('No daiaIdPrefix setting found, using default: ppn:');
181            $this->daiaIdPrefix = 'ppn:';
182        }
183        if (isset($this->config['DAIA']['multiQuery'])) {
184            $this->multiQuery = $this->config['DAIA']['multiQuery'];
185        } else {
186            $this->debug('No multiQuery setting found, using default: false');
187        }
188        if (isset($this->config['DAIA']['daiaContentTypes'])) {
189            $this->contentTypesResponse = $this->config['DAIA']['daiaContentTypes'];
190        } else {
191            $this->debug('No ContentTypes for response defined. Accepting any.');
192        }
193        if (isset($this->config['DAIA']['daiaCache'])) {
194            $this->daiaCacheEnabled = $this->config['DAIA']['daiaCache'];
195        } else {
196            $this->debug('Caching not enabled, disabling it by default.');
197        }
198        if (
199            isset($this->config['General'])
200            && isset($this->config['General']['cacheLifetime'])
201        ) {
202            $this->cacheLifetime = $this->config['General']['cacheLifetime'];
203        } else {
204            $this->debug(
205                'Cache lifetime not set, using VuFind\ILS\Driver\AbstractBase ' .
206                'default value.'
207            );
208        }
209    }
210
211    /**
212     * DAIA specific override of method to ensure uniform cache keys for cached
213     * VuFind objects.
214     *
215     * @param string|null $suffix Optional suffix that will get appended to the
216     * object class name calling getCacheKey()
217     *
218     * @return string
219     */
220    protected function getCacheKey($suffix = null)
221    {
222        return $this->getBaseCacheKey(md5($this->baseUrl) . $suffix);
223    }
224
225    /**
226     * Public Function which retrieves renew, hold and cancel settings from the
227     * driver ini file.
228     *
229     * @param string $function The name of the feature to be checked
230     * @param array  $params   Optional feature-specific parameters (array)
231     *
232     * @return array An array with key-value pairs.
233     *
234     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
235     */
236    public function getConfig($function, $params = [])
237    {
238        return $this->config[$function] ?? false;
239    }
240
241    /**
242     * Get Hold Link
243     *
244     * The goal for this method is to return a URL to a "place hold" web page on
245     * the ILS OPAC. This is used for ILSs that do not support an API or method
246     * to place Holds.
247     *
248     * @param string $id      The id of the bib record
249     * @param array  $details Item details from getHoldings return array
250     *
251     * @return string         URL to ILS's OPAC's place hold screen.
252     *
253     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
254     */
255    public function getHoldLink($id, $details)
256    {
257        return (isset($details['ilslink']) && $details['ilslink'] != '')
258            ? $details['ilslink']
259            : null;
260    }
261
262    /**
263     * Get Status
264     *
265     * This is responsible for retrieving the status information of a certain
266     * record.
267     *
268     * @param string $id The record id to retrieve the holdings for
269     *
270     * @return mixed     On success, an associative array with the following keys:
271     * id, availability (boolean), status, location, reserve, callnumber.
272     */
273    public function getStatus($id)
274    {
275        // check ids for existing availability data in cache and skip these ids
276        if (
277            $this->daiaCacheEnabled
278            && $item = $this->getCachedData($this->generateURI($id))
279        ) {
280            if ($item != null) {
281                return $item;
282            }
283        }
284
285        // let's retrieve the DAIA document by URI
286        try {
287            $rawResult = $this->doHTTPRequest($this->generateURI($id));
288            // extract the DAIA document for the current id from the
289            // HTTPRequest's result
290            $doc = $this->extractDaiaDoc($id, $rawResult);
291            if (null !== $doc) {
292                // parse the extracted DAIA document and return the status info
293                $data = $this->parseDaiaDoc($id, $doc);
294                // cache the status information
295                if ($this->daiaCacheEnabled) {
296                    $this->putCachedData($this->generateURI($id), $data);
297                }
298                return $data;
299            }
300        } catch (ILSException $e) {
301            $this->debug($e->getMessage());
302        }
303
304        return [];
305    }
306
307    /**
308     * Get Statuses
309     *
310     * This is responsible for retrieving the status information for a
311     * collection of records.
312     * As the DAIA Query API supports querying multiple ids simultaneously
313     * (all ids divided by "|") getStatuses(ids) would call getStatus(id) only
314     * once, id containing the list of ids to be retrieved. This would cause some
315     * trouble as the list of ids does not necessarily correspond to the VuFind
316     * Record-id. Therefore getStatuses(ids) has its own logic for multiQuery-support
317     * and performs the HTTPRequest itself, retrieving one DAIA response for all ids
318     * and uses helper functions to split this one response into documents
319     * corresponding to the queried ids.
320     *
321     * @param array $ids The array of record ids to retrieve the status for
322     *
323     * @return array    An array of status information values on success.
324     */
325    public function getStatuses($ids)
326    {
327        $status = [];
328
329        // check cache for given ids and skip these ids if availability data is found
330        foreach ($ids as $key => $id) {
331            if (
332                $this->daiaCacheEnabled
333                && $item = $this->getCachedData($this->generateURI($id))
334            ) {
335                if ($item != null) {
336                    $status[] = $item;
337                    unset($ids[$key]);
338                }
339            }
340        }
341
342        // only query DAIA service if we have some ids left
343        if (count($ids) > 0) {
344            try {
345                if ($this->multiQuery) {
346                    // perform one DAIA query with multiple URIs
347                    $rawResult = $this
348                        ->doHTTPRequest($this->generateMultiURIs($ids));
349                    // the id used in VuFind can differ from the document-URI
350                    // (depending on how the URI is generated)
351                    foreach ($ids as $id) {
352                        // it is assumed that each DAIA document has a unique URI,
353                        // so get the document with the corresponding id
354                        $doc = $this->extractDaiaDoc($id, $rawResult);
355                        if (null !== $doc) {
356                            // a document with the corresponding id exists, which
357                            // means we got status information for that record
358                            $data = $this->parseDaiaDoc($id, $doc);
359                            // cache the status information
360                            if ($this->daiaCacheEnabled) {
361                                $this->putCachedData($this->generateURI($id), $data);
362                            }
363                            $status[] = $data;
364                        }
365                        unset($doc);
366                    }
367                } else {
368                    // multiQuery is not supported, so retrieve DAIA documents one by
369                    // one
370                    foreach ($ids as $id) {
371                        $rawResult = $this->doHTTPRequest($this->generateURI($id));
372                        // extract the DAIA document for the current id from the
373                        // HTTPRequest's result
374                        $doc = $this->extractDaiaDoc($id, $rawResult);
375                        if (null !== $doc) {
376                            // parse the extracted DAIA document and save the status
377                            // info
378                            $data = $this->parseDaiaDoc($id, $doc);
379                            // cache the status information
380                            if ($this->daiaCacheEnabled) {
381                                $this->putCachedData($this->generateURI($id), $data);
382                            }
383                            $status[] = $data;
384                        }
385                    }
386                }
387            } catch (ILSException $e) {
388                $this->debug($e->getMessage());
389            }
390        }
391        return $status;
392    }
393
394    /**
395     * Get Holding
396     *
397     * This is responsible for retrieving the holding information of a certain
398     * record.
399     *
400     * @param string $id      The record id to retrieve the holdings for
401     * @param array  $patron  Patron data
402     * @param array  $options Extra options (not currently used)
403     *
404     * @return array         On success, an associative array with the following
405     * keys: id, availability (boolean), status, location, reserve, callnumber,
406     * duedate, number, barcode.
407     *
408     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
409     */
410    public function getHolding($id, array $patron = null, array $options = [])
411    {
412        return $this->getStatus($id);
413    }
414
415    /**
416     * Get Purchase History
417     *
418     * This is responsible for retrieving the acquisitions history data for the
419     * specific record (usually recently received issues of a serial).
420     *
421     * @param string $id The record id to retrieve the info for
422     *
423     * @throws ILSException
424     * @return array     An array with the acquisitions data on success.
425     */
426    public function getPurchaseHistory($id)
427    {
428        return [];
429    }
430
431    /**
432     * Support method to handle date uniformly
433     *
434     * @param string $date String representing a date
435     *
436     * @return string Formatted date
437     */
438    protected function convertDate($date)
439    {
440        try {
441            return $this->dateConverter
442                ->convertToDisplayDate('Y-m-d', $date);
443        } catch (\Exception $e) {
444            $this->debug('Date conversion failed: ' . $e->getMessage());
445            return '';
446        }
447    }
448
449    /**
450     * Support method to handle datetime uniformly
451     *
452     * @param string $datetime String representing a datetime
453     *
454     * @return string Formatted datetime
455     */
456    protected function convertDatetime($datetime)
457    {
458        return $this->convertDate($datetime);
459    }
460
461    /**
462     * Perform an HTTP request.
463     *
464     * @param string $id id for query in daia
465     *
466     * @return xml or json object
467     * @throws ILSException
468     */
469    protected function doHTTPRequest($id)
470    {
471        $http_headers = [
472            'Content-type: ' . $this->contentTypesRequest[$this->daiaResponseFormat],
473            'Accept: ' . $this->contentTypesRequest[$this->daiaResponseFormat],
474        ];
475
476        $params = [
477            'id' => $id,
478            'format' => $this->daiaResponseFormat,
479        ];
480
481        try {
482            $result = $this->httpService->get(
483                $this->baseUrl,
484                $params,
485                $this->daiaTimeout,
486                $http_headers
487            );
488        } catch (\Exception $e) {
489            $msg = 'HTTP request exited with Exception ' . $e->getMessage() .
490                ' for record: ' . $id;
491            $this->throwAsIlsException($e, $msg);
492        }
493
494        if (!$result->isSuccess()) {
495            throw new ILSException(
496                'HTTP status ' . $result->getStatusCode() .
497                ' received, retrieving availability information for record: ' . $id
498            );
499        }
500
501        // check if result matches daiaResponseFormat
502        if ($this->contentTypesResponse != null) {
503            if ($this->contentTypesResponse[$this->daiaResponseFormat]) {
504                $contentTypesResponse = array_map(
505                    'trim',
506                    explode(
507                        ',',
508                        $this->contentTypesResponse[$this->daiaResponseFormat]
509                    )
510                );
511                [$responseMediaType] = array_pad(
512                    explode(
513                        ';',
514                        $result->getHeaders()->get('Content-type')->getFieldValue(),
515                        2
516                    ),
517                    2,
518                    null
519                ); // workaround to avoid notices if encoding is not set in header
520                if (!in_array(trim($responseMediaType), $contentTypesResponse)) {
521                    throw new ILSException(
522                        'DAIA-ResponseFormat not supported. Received: ' .
523                        $responseMediaType . ' - ' .
524                        'Expected: ' .
525                        $this->contentTypesResponse[$this->daiaResponseFormat]
526                    );
527                }
528            }
529        }
530
531        return $result->getBody();
532    }
533
534    /**
535     * Generate a DAIA URI necessary for the query
536     *
537     * @param string $id Id of the record whose DAIA document should be queried
538     *
539     * @return string     URI of the DAIA document
540     *
541     * @see http://gbv.github.io/daia/daia.html#query-parameters
542     */
543    protected function generateURI($id)
544    {
545        return $this->daiaIdPrefix . $id;
546    }
547
548    /**
549     * Combine several ids to DAIA Query API conform URIs
550     *
551     * @param array $ids Array of ids which shall be converted into URIs and
552     *                  combined for querying multiple DAIA documents.
553     *
554     * @return string   Combined URIs (delimited by "|")
555     *
556     * @see http://gbv.github.io/daia/daia.html#query-parameters
557     */
558    protected function generateMultiURIs($ids)
559    {
560        $multiURI = '';
561        foreach ($ids as $id) {
562            $multiURI .= $this->generateURI($id) . '|';
563        }
564        return rtrim($multiURI, '|');
565    }
566
567    /**
568     * Parse a DAIA document depending on its type.
569     *
570     * Parse a DAIA document depending on its type and return a VuFind
571     * compatible array of status information.
572     * Supported types are:
573     *      - array (for JSON results)
574     *
575     * @param string $id      Record Id corresponding to the DAIA document
576     * @param mixed  $daiaDoc The DAIA document, only array is supported
577     *
578     * @return array An array with status information for the record
579     * @throws ILSException
580     */
581    protected function parseDaiaDoc($id, $daiaDoc)
582    {
583        if (is_array($daiaDoc)) {
584            return $this->parseDaiaArray($id, $daiaDoc);
585        } else {
586            throw new ILSException(
587                'Unsupported document type (did not match Array or DOMNode).'
588            );
589        }
590    }
591
592    /**
593     * Extract a DAIA document identified by an id
594     *
595     * This method loops through all the existing DAIA document-elements in
596     * the given DAIA response and returns the first document whose id matches
597     * the given id.
598     *
599     * @param string $id           Record Id of the DAIA document in question.
600     * @param string $daiaResponse Raw response from DAIA request.
601     *
602     * @return Array|DOMNode|null   The DAIA document identified by id and
603     *                                  type depending on daiaResponseFormat.
604     * @throws ILSException
605     */
606    protected function extractDaiaDoc($id, $daiaResponse)
607    {
608        $docs = [];
609        if ($this->daiaResponseFormat == 'xml') {
610            try {
611                $docs = $this->convertDaiaXmlToJson($daiaResponse);
612            } catch (\Exception $e) {
613                $this->throwAsIlsException($e);
614            }
615        } elseif ($this->daiaResponseFormat == 'json') {
616            $docs = json_decode($daiaResponse, true);
617        }
618
619        if (count($docs)) {
620            // check for error messages and write those to log
621            if (isset($docs['message'])) {
622                $this->logMessages($docs['message'], 'document');
623            }
624
625            // do DAIA documents exist?
626            if (isset($docs['document']) && $this->multiQuery) {
627                // now loop through the found DAIA documents
628                foreach ($docs['document'] as $doc) {
629                    // DAIA documents should use URIs as value for id
630                    if (
631                        isset($doc['id'])
632                        && $doc['id'] == $this->generateURI($id)
633                    ) {
634                        // we've found the document element with the matching URI
635                        // if the document has an item, then we return it
636                        if (isset($doc['item'])) {
637                            return $doc;
638                        }
639                    }
640                }
641            } elseif (isset($docs['document'])) {
642                // since a document exists but multiQuery is disabled, the first
643                // document is returned if it contains an item
644                $doc = array_shift($docs['document']);
645                if (isset($doc['item'])) {
646                    return $doc;
647                }
648            }
649            // no (id matching) document element found
650            return null;
651        } else {
652            throw new ILSException('Unsupported document format.');
653        }
654    }
655
656    /**
657     * Converts a DAIA XML response to an array identical with a DAIA JSON response
658     * for the sent query.
659     *
660     * @param string $daiaResponse Response in XML format from DAIA service
661     *
662     * @return mixed
663     */
664    protected function convertDaiaXmlToJson($daiaResponse)
665    {
666        $dom = new DOMDocument();
667        $dom->loadXML($daiaResponse);
668
669        // prepare DOMDocument as json_encode does not support save attributes if
670        // elements have values (see http://stackoverflow.com/a/20506281/2115462)
671        $prepare = function ($domNode) use (&$prepare) {
672            foreach ($domNode->childNodes as $node) {
673                if ($node->hasChildNodes()) {
674                    $prepare($node);
675                } else {
676                    if (
677                        ($domNode->hasAttributes() && strlen($domNode->nodeValue))
678                        || (in_array(
679                            $domNode->nodeName,
680                            ['storage', 'limitation', 'department', 'institution']
681                        ) && strlen($domNode->nodeValue))
682                    ) {
683                        if (trim($node->textContent)) {
684                            $domNode->setAttribute('content', $node->textContent);
685                            $node->nodeValue = '';
686                        }
687                    }
688                }
689            }
690        };
691        $prepare($dom);
692
693        // now let json_encode/decode convert XML into an array
694        $daiaArray = json_decode(
695            json_encode(simplexml_load_string($dom->saveXML())),
696            true
697        );
698
699        // merge @attributes fields in parent array
700        $merge = function ($array) use (&$merge) {
701            foreach ($array as $key => $value) {
702                if (is_array($value)) {
703                    $value = $merge($value);
704                }
705                if ($key === '@attributes') {
706                    $array = array_merge($array, $value);
707                    unset($array[$key]);
708                } else {
709                    $array[$key] = $value;
710                }
711            }
712            return $array;
713        };
714        $daiaArray = $merge($daiaArray);
715
716        // restructure the array, moving single elements to their parent's index [0]
717        $restructure = function ($array) use (&$restructure) {
718            $elements = [
719                'document', 'item', 'available', 'unavailable', 'limitation',
720                'message',
721            ];
722            foreach ($array as $key => $value) {
723                if (is_array($value)) {
724                    $value = $restructure($value);
725                }
726                if (
727                    in_array($key, $elements, true)
728                    && !isset($array[$key][0])
729                ) {
730                    unset($array[$key]);
731                    $array[$key][] = $value;
732                } else {
733                    $array[$key] = $value;
734                }
735            }
736            return $array;
737        };
738        $daiaArray = $restructure($daiaArray);
739
740        return $daiaArray;
741    }
742
743    /**
744     * Parse an array with DAIA status information.
745     *
746     * @param string $id        Record id for the DAIA array.
747     * @param array  $daiaArray Array with raw DAIA status information.
748     *
749     * @return array            Array with VuFind compatible status information.
750     */
751    protected function parseDaiaArray($id, $daiaArray)
752    {
753        $result = [];
754        $doc_id = null;
755        $doc_href = null;
756        if (isset($daiaArray['id'])) {
757            $doc_id = $daiaArray['id'];
758        }
759        if (isset($daiaArray['href'])) {
760            // url of the document (not needed for VuFind)
761            $doc_href = $daiaArray['href'];
762        }
763        if (isset($daiaArray['message'])) {
764            // log messages for debugging
765            $this->logMessages($daiaArray['message'], 'document');
766        }
767        // if one or more items exist, iterate and build result-item
768        if (isset($daiaArray['item']) && is_array($daiaArray['item'])) {
769            $number = 0;
770            foreach ($daiaArray['item'] as $item) {
771                $result_item = [];
772                $result_item['id'] = $id;
773                // custom DAIA field
774                $result_item['doc_id'] = $doc_id;
775                $result_item['item_id'] = $item['id'];
776                // custom DAIA field used in getHoldLink()
777                $result_item['ilslink']
778                    = ($item['href'] ?? $doc_href);
779                // count items
780                $number++;
781                $result_item['number'] = $this->getItemNumber($item, $number);
782                // set default value for barcode
783                $result_item['barcode'] = $this->getItemBarcode($item);
784                // set default value for reserve
785                $result_item['reserve'] = $this->getItemReserveStatus($item);
786                // get callnumber
787                $result_item['callnumber'] = $this->getItemCallnumber($item);
788                // get location
789                $result_item['location'] = $this->getItemDepartment($item);
790                // custom DAIA field
791                $result_item['locationid'] = $this->getItemDepartmentId($item);
792                // get location link
793                $result_item['locationhref'] = $this->getItemDepartmentLink($item);
794                // custom DAIA field
795                $result_item['storage'] = $this->getItemStorage($item);
796                // custom DAIA field
797                $result_item['storageid'] = $this->getItemStorageId($item);
798                // custom DAIA field
799                $result_item['storagehref'] = $this->getItemStorageLink($item);
800                // status and availability will be calculated in own function
801                $result_item = $this->getItemStatus($item) + $result_item;
802                // add result_item to the result array
803                $result[] = $result_item;
804            } // end iteration on item
805        }
806
807        return $result;
808    }
809
810    /**
811     * Returns an array with status information for provided item.
812     *
813     * @param array $item Array with DAIA item data
814     *
815     * @return array
816     */
817    protected function getItemStatus($item)
818    {
819        $return = [];
820        $availability = false;
821        $duedate = null;
822        $serviceLink = '';
823        $queue = '';
824        $item_notes = [];
825        $item_limitation_types = [];
826        $services = [];
827
828        if (isset($item['available'])) {
829            // check if item is loanable or presentation
830            foreach ($item['available'] as $available) {
831                if (
832                    isset($available['service'])
833                    && in_array($available['service'], ['loan', 'presentation'])
834                ) {
835                    $services['available'][] = $available['service'];
836                }
837                // attribute service can be set once or not
838                if (
839                    isset($available['service'])
840                    && in_array(
841                        $available['service'],
842                        ['loan', 'presentation', 'openaccess']
843                    )
844                ) {
845                    // set item available if service is loan, presentation or
846                    // openaccess
847                    $availability = true;
848                    if (
849                        $available['service'] == 'loan'
850                        && isset($available['href'])
851                    ) {
852                        // save the link to the ils if we have a href for loan
853                        // service
854                        $serviceLink = $available['href'];
855                    }
856                }
857
858                // use limitation element for status string
859                if (isset($available['limitation'])) {
860                    $item_notes = array_merge(
861                        $item_notes,
862                        $this->getItemLimitationContent($available['limitation'])
863                    );
864                    $item_limitation_types = array_merge(
865                        $item_limitation_types,
866                        $this->getItemLimitationTypes($available['limitation'])
867                    );
868                }
869
870                // log messages for debugging
871                if (isset($available['message'])) {
872                    $this->logMessages($available['message'], 'item->available');
873                }
874            }
875        }
876
877        if (isset($item['unavailable'])) {
878            foreach ($item['unavailable'] as $unavailable) {
879                if (
880                    isset($unavailable['service'])
881                    && in_array($unavailable['service'], ['loan', 'presentation'])
882                ) {
883                    $services['unavailable'][] = $unavailable['service'];
884                }
885                // attribute service can be set once or not
886                if (
887                    isset($unavailable['service'])
888                    && in_array(
889                        $unavailable['service'],
890                        ['loan', 'presentation', 'openaccess']
891                    )
892                ) {
893                    if (
894                        $unavailable['service'] == 'loan'
895                        && isset($unavailable['href'])
896                    ) {
897                        //save the link to the ils if we have a href for loan service
898                        $serviceLink = $unavailable['href'];
899                    }
900
901                    // use limitation element for status string
902                    if (isset($unavailable['limitation'])) {
903                        $item_notes = array_merge(
904                            $item_notes,
905                            $this->getItemLimitationContent(
906                                $unavailable['limitation']
907                            )
908                        );
909                        $item_limitation_types = array_merge(
910                            $item_limitation_types,
911                            $this->getItemLimitationTypes($unavailable['limitation'])
912                        );
913                    }
914                }
915                // attribute expected is mandatory for unavailable element
916                if (!empty($unavailable['expected'])) {
917                    try {
918                        $duedate = $this->dateConverter
919                            ->convertToDisplayDate(
920                                'Y-m-d',
921                                $unavailable['expected']
922                            );
923                    } catch (\Exception $e) {
924                        $this->debug('Date conversion failed: ' . $e->getMessage());
925                        $duedate = null;
926                    }
927                }
928
929                // attribute queue can be set
930                if (isset($unavailable['queue'])) {
931                    $queue = $unavailable['queue'];
932                }
933
934                // log messages for debugging
935                if (isset($unavailable['message'])) {
936                    $this->logMessages($unavailable['message'], 'item->unavailable');
937                }
938            }
939        }
940
941        /*'returnDate' => '', // false if not recently returned(?)*/
942
943        if (!empty($serviceLink)) {
944            $return['ilslink'] = $serviceLink;
945        }
946
947        $return['item_notes']      = $item_notes;
948        $return['status']          = $this->getStatusString($item);
949        $return['availability']    = $availability;
950        $return['duedate']         = $duedate;
951        $return['requests_placed'] = $queue;
952        $return['services']        = $this->getAvailableItemServices($services);
953
954        // In this DAIA driver implementation addLink and is_holdable are assumed
955        // Boolean as patron based availability requires either a patron-id or -type.
956        // This should be handled in a custom DAIA driver
957        $return['addLink']     = $this->checkIsRecallable($item);
958        $return['is_holdable'] = $this->checkIsRecallable($item);
959        $return['holdtype']    = $this->getHoldType($item);
960
961        // Check if we the item is available for storage retrieval request if it is
962        // not holdable.
963        $return['addStorageRetrievalRequestLink'] = !$return['is_holdable']
964            ? $this->checkIsStorageRetrievalRequest($item) : false;
965
966        // add a custom Field to allow passing custom DAIA data to the frontend in
967        // order to use it for more precise display of availability
968        $return['customData']      = $this->getCustomData($item);
969
970        $return['limitation_types'] = $item_limitation_types;
971
972        return $return;
973    }
974
975    /**
976     * Helper function to allow custom data in status array.
977     *
978     * @param array $item Array with DAIA item data
979     *
980     * @return array
981     */
982    protected function getCustomData($item)
983    {
984        return [];
985    }
986
987    /**
988     * Helper function to return an appropriate status string for current item.
989     *
990     * @param array $item Array with DAIA item data
991     *
992     * @return string
993     */
994    protected function getStatusString($item)
995    {
996        // status cannot be null as this will crash the translator
997        return '';
998    }
999
1000    /**
1001     * Helper function to determine if item is recallable.
1002     * DAIA does not genuinly allow distinguishing between holdable and recallable
1003     * items. This could be achieved by usage of limitations but this would not be
1004     * shared functionality between different DAIA implementations (thus should be
1005     * implemented in custom drivers). Therefore this returns whether an item
1006     * is recallable based on unavailable services and the existence of an href.
1007     *
1008     * @param array $item Array with DAIA item data
1009     *
1010     * @return bool
1011     */
1012    protected function checkIsRecallable($item)
1013    {
1014        // This basic implementation checks the item for being unavailable for loan
1015        // and presentation but with an existing href (as a flag for further action).
1016        $services = ['available' => [], 'unavailable' => []];
1017        $href = false;
1018        if (isset($item['available'])) {
1019            // check if item is loanable or presentation
1020            foreach ($item['available'] as $available) {
1021                if (
1022                    isset($available['service'])
1023                    && in_array($available['service'], ['loan', 'presentation'])
1024                ) {
1025                    $services['available'][] = $available['service'];
1026                }
1027            }
1028        }
1029
1030        if (isset($item['unavailable'])) {
1031            foreach ($item['unavailable'] as $unavailable) {
1032                if (
1033                    isset($unavailable['service'])
1034                    && in_array($unavailable['service'], ['loan', 'presentation'])
1035                ) {
1036                    $services['unavailable'][] = $unavailable['service'];
1037                    // attribute href is used to determine whether item is recallable
1038                    // or not
1039                    $href = isset($unavailable['href']) ? true : $href;
1040                }
1041            }
1042        }
1043
1044        // Check if we have at least one service unavailable and a href field is set
1045        // (either as flag or as actual value for the next action).
1046        return $href && count(
1047            array_diff($services['unavailable'], $services['available'])
1048        );
1049    }
1050
1051    /**
1052     * Helper function to determine if the item is available as storage retrieval.
1053     *
1054     * @param array $item Array with DAIA item data
1055     *
1056     * @return bool
1057     */
1058    protected function checkIsStorageRetrievalRequest($item)
1059    {
1060        // This basic implementation checks the item for being available for loan
1061        // and presentation but with an existing href (as a flag for further action).
1062        $services = ['available' => [], 'unavailable' => []];
1063        $href = false;
1064        if (isset($item['available'])) {
1065            // check if item is loanable or presentation
1066            foreach ($item['available'] as $available) {
1067                if (
1068                    isset($available['service'])
1069                    && in_array($available['service'], ['loan', 'presentation'])
1070                ) {
1071                    $services['available'][] = $available['service'];
1072                    // attribute href is used to determine whether item is
1073                    // requestable or not
1074                    $href = isset($available['href']) ? true : $href;
1075                }
1076            }
1077        }
1078
1079        if (isset($item['unavailable'])) {
1080            foreach ($item['unavailable'] as $unavailable) {
1081                if (
1082                    isset($unavailable['service'])
1083                    && in_array($unavailable['service'], ['loan', 'presentation'])
1084                ) {
1085                    $services['unavailable'][] = $unavailable['service'];
1086                }
1087            }
1088        }
1089
1090        // Check if we have at least one service unavailable and a href field is set
1091        // (either as flag or as actual value for the next action).
1092        return $href && count(
1093            array_diff($services['available'], $services['unavailable'])
1094        );
1095    }
1096
1097    /**
1098     * Helper function to determine the holdtype available for current item.
1099     * DAIA does not genuinly allow distinguishing between holdable and recallable
1100     * items. This could be achieved by usage of limitations but this would not be
1101     * shared functionality between different DAIA implementations (thus should be
1102     * implemented in custom drivers). Therefore getHoldType always returns recall.
1103     *
1104     * @param array $item Array with DAIA item data
1105     *
1106     * @return string 'recall'|null
1107     */
1108    protected function getHoldType($item)
1109    {
1110        // return holdtype (hold, recall or block if patron is not allowed) for item
1111        return $this->checkIsRecallable($item) ? 'recall' : null;
1112    }
1113
1114    /**
1115     * Returns the evaluated value of the provided limitation element
1116     *
1117     * @param array $limitations Array with DAIA limitation data
1118     *
1119     * @return array
1120     */
1121    protected function getItemLimitation($limitations)
1122    {
1123        $itemLimitation = [];
1124        foreach ($limitations as $limitation) {
1125            // return the first limitation with content set
1126            if (isset($limitation['content'])) {
1127                $itemLimitation[] = $limitation['content'];
1128            }
1129        }
1130        return $itemLimitation;
1131    }
1132
1133    /**
1134     * Returns the value of item.department.content (e.g. to be used in VuFind
1135     * getStatus/getHolding array as location)
1136     *
1137     * @param array $item Array with DAIA item data
1138     *
1139     * @return string
1140     */
1141    protected function getItemDepartment($item)
1142    {
1143        return isset($item['department']) && isset($item['department']['content'])
1144        && !empty($item['department']['content'])
1145            ? $item['department']['content']
1146            : 'Unknown';
1147    }
1148
1149    /**
1150     * Returns the value of item.department.id (e.g. to be used in VuFind
1151     * getStatus/getHolding array as location)
1152     *
1153     * @param array $item Array with DAIA item data
1154     *
1155     * @return string
1156     */
1157    protected function getItemDepartmentId($item)
1158    {
1159        return isset($item['department']) && isset($item['department']['id'])
1160            ? $item['department']['id'] : '';
1161    }
1162
1163    /**
1164     * Returns the value of item.department.href (e.g. to be used in VuFind
1165     * getStatus/getHolding array for linking the location)
1166     *
1167     * @param array $item Array with DAIA item data
1168     *
1169     * @return string
1170     */
1171    protected function getItemDepartmentLink($item)
1172    {
1173        return $item['department']['href'] ?? false;
1174    }
1175
1176    /**
1177     * Returns the value of item.storage.content (e.g. to be used in VuFind
1178     * getStatus/getHolding array as location)
1179     *
1180     * @param array $item Array with DAIA item data
1181     *
1182     * @return string
1183     */
1184    protected function getItemStorage($item)
1185    {
1186        return isset($item['storage']) && isset($item['storage']['content'])
1187        && !empty($item['storage']['content'])
1188            ? $item['storage']['content']
1189            : 'Unknown';
1190    }
1191
1192    /**
1193     * Returns the value of item.storage.id (e.g. to be used in VuFind
1194     * getStatus/getHolding array as location)
1195     *
1196     * @param array $item Array with DAIA item data
1197     *
1198     * @return string
1199     */
1200    protected function getItemStorageId($item)
1201    {
1202        return isset($item['storage']) && isset($item['storage']['id'])
1203            ? $item['storage']['id'] : '';
1204    }
1205
1206    /**
1207     * Returns the value of item.storage.href (e.g. to be used in VuFind
1208     * getStatus/getHolding array for linking the location)
1209     *
1210     * @param array $item Array with DAIA item data
1211     *
1212     * @return string
1213     */
1214    protected function getItemStorageLink($item)
1215    {
1216        return isset($item['storage']) && isset($item['storage']['href'])
1217            ? $item['storage']['href'] : '';
1218    }
1219
1220    /**
1221     * Returns the evaluated values of the provided limitations element
1222     *
1223     * @param array $limitations Array with DAIA limitation data
1224     *
1225     * @return array
1226     */
1227    protected function getItemLimitationContent($limitations)
1228    {
1229        $itemLimitationContent = [];
1230        foreach ($limitations as $limitation) {
1231            // return the first limitation with content set
1232            if (isset($limitation['content'])) {
1233                $itemLimitationContent[] = $limitation['content'];
1234            }
1235        }
1236        return $itemLimitationContent;
1237    }
1238
1239    /**
1240     * Returns the evaluated values of the provided limitations element
1241     *
1242     * @param array $limitations Array with DAIA limitation data
1243     *
1244     * @return array
1245     */
1246    protected function getItemLimitationTypes($limitations)
1247    {
1248        $itemLimitationTypes = [];
1249        foreach ($limitations as $limitation) {
1250            // return the first limitation with content set
1251            if (isset($limitation['id'])) {
1252                $itemLimitationTypes[] = $limitation['id'];
1253            }
1254        }
1255        return $itemLimitationTypes;
1256    }
1257
1258    /**
1259     * Returns the value for "number" in VuFind getStatus/getHolding array
1260     *
1261     * @param array $item    Array with DAIA item data
1262     * @param int   $counter Integer counting items as alternative return value
1263     *
1264     * @return mixed
1265     */
1266    protected function getItemNumber($item, $counter)
1267    {
1268        return $counter;
1269    }
1270
1271    /**
1272     * Returns the value for "location" in VuFind getStatus/getHolding array
1273     *
1274     * @param array $item Array with DAIA item data
1275     *
1276     * @return string
1277     */
1278    protected function getItemBarcode($item)
1279    {
1280        return '1';
1281    }
1282
1283    /**
1284     * Returns the value for "reserve" in VuFind getStatus/getHolding array
1285     *
1286     * @param array $item Array with DAIA item data
1287     *
1288     * @return string
1289     */
1290    protected function getItemReserveStatus($item)
1291    {
1292        return 'N';
1293    }
1294
1295    /**
1296     * Returns the value for "callnumber" in VuFind getStatus/getHolding array
1297     *
1298     * @param array $item Array with DAIA item data
1299     *
1300     * @return string
1301     */
1302    protected function getItemCallnumber($item)
1303    {
1304        return isset($item['label']) && !empty($item['label'])
1305            ? $item['label']
1306            : 'Unknown';
1307    }
1308
1309    /**
1310     * Returns the available services of the given set of available and unavailable
1311     * services
1312     *
1313     * @param array $services Array with DAIA services available/unavailable
1314     *
1315     * @return array
1316     */
1317    protected function getAvailableItemServices($services)
1318    {
1319        $availableServices = [];
1320        if (isset($services['available'])) {
1321            foreach ($services['available'] as $service) {
1322                if (
1323                    !isset($services['unavailable'])
1324                    || !in_array($service, $services['unavailable'])
1325                ) {
1326                    $availableServices[] = $service;
1327                }
1328            }
1329        }
1330        return array_intersect(['loan', 'presentation'], $availableServices);
1331    }
1332
1333    /**
1334     * Logs content of message elements in DAIA response for debugging
1335     *
1336     * @param array  $messages Array with message elements to be logged
1337     * @param string $context  Description of current message context
1338     *
1339     * @return void
1340     */
1341    protected function logMessages($messages, $context)
1342    {
1343        foreach ($messages as $message) {
1344            if (isset($message['content'])) {
1345                $this->debug(
1346                    'Message in DAIA response (' . (string)$context . '): ' .
1347                    $message['content']
1348                );
1349            }
1350        }
1351    }
1352}