diff --git a/config/vufind/DAIA.ini b/config/vufind/DAIA.ini index c1d828105cdb43a1add459a4646b8cfd8a2e0e07..d452c136f7727446a8d30fb18ae5d5f61c7553f1 100644 --- a/config/vufind/DAIA.ini +++ b/config/vufind/DAIA.ini @@ -1,2 +1,36 @@ -[Global] -baseUrl = [your DAIA server base url] \ No newline at end of file +; DAIA driver expects a DAIA Query API as specified in: +; http://gbv.github.io/daiaspec/daia.html#query-api +; +; The settings in the [DAIA] section will be used for all DAIA requests. +; The name of this section got refactored with VuFind 2.4, although the old +; configuration using the [Global] section still works, it should be replaced +; with the new [DAIA] section. +; Note: Settings for daiaResponseFormat and daiaIdPrefix are not supported if +; a pre VuFind 2.4 configuration with the [Global] section is used. +; i.e.: +; [Global] +; baseUrl = [your DAIA server base url] +; daiaIdPrefix = [this setting will have no effect] +; daiaResponseFormat = [this setting will have no effect] +; +; A default DAIA call looks like this: +; https://daia.myuniversity.edu/?id=ppn:12345678&format=json +; +; This default DAIA call would be configured as: +; [DAIA] +; baseUrl = https://daia.myuniversity.edu +; daiaidprefix = ppn: +; daiaResponseFormat = json +; + +[DAIA] +; The base URL for the DAIA webservice. +baseUrl = [your DAIA server base url] + +; The prefix prepended to the VuFind record Id resulting in the document URI +; used for the DAIA request (default = ppn:) (the prefix usually defines the +; field which the DAIA server uses for the loookup - e.g. ppn: or isbn:). +;daiaIdPrefix = "ppn:" + +; Set the requested DAIA response format: xml (default), json +;daiaResponseFormat = xml diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 2da7e753645d3b91de0daf6b8cc5d9a536c54c7e..417c4abd35b50459786875aaf6374865350ca3bf 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -124,6 +124,8 @@ lifetime = 3600 ; Session lasts for 1 hour ; w/ RESTful web services), XCNCIP2 (for XC NCIP Tookit v2.x) ; Note: Unicorn users should visit the vufind-unicorn project for more details: ; http://code.google.com/p/vufind-unicorn/ +; Note: DAIA supports XML or JSON results (since release 2.4). +; For details look in the driver code and the corresponding ini file. ; ; If you haven't set up your ILS yet, two fake drivers are available for testing ; purposes. "Sample" is fast but does very little; "Demo" simulates more @@ -783,6 +785,9 @@ url = https://www.myendnoteweb.com/EndNoteWeb.html [Http] ;sslcapath = "/etc/ssl/certs" +; Using a curl Adapter instead of the the defaultAdapter (Socket) +; adapter = 'Zend\Http\Client\Adapter\Curl' + ; Spelling Suggestions ; ; Note: These settings affect the VuFind side of spelling suggestions; you diff --git a/module/VuFind/src/VuFind/ILS/Driver/DAIA.php b/module/VuFind/src/VuFind/ILS/Driver/DAIA.php index c98017ef1f7576663c805cd3b7fcd279702cb5ee..835af30c1edcebff39b455245e6c793f8a41db62 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/DAIA.php +++ b/module/VuFind/src/VuFind/ILS/Driver/DAIA.php @@ -3,10 +3,11 @@ * ILS Driver for VuFind to query availability information via DAIA. * * Based on the proof-of-concept-driver by Till Kinstler, GBV. + * Relaunch of the daia driver developed by Oliver Goldschmidt. * * PHP version 5 * - * Copyright (C) Oliver Goldschmidt 2010. + * Copyright (C) Jochen Lienhard 2014. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -23,34 +24,61 @@ * * @category VuFind2 * @package ILS_Drivers + * @author Jochen Lienhard <lienhard@ub.uni-freiburg.de> * @author Oliver Goldschmidt <o.goldschmidt@tu-harburg.de> + * @author André Lahmann <lahmann@ub.uni-leipzig.de> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki */ namespace VuFind\ILS\Driver; -use DOMDocument, VuFind\Exception\ILS as ILSException; +use DOMDocument, VuFind\Exception\ILS as ILSException, + VuFindHttp\HttpServiceAwareInterface as HttpServiceAwareInterface, + Zend\Log\LoggerAwareInterface as LoggerAwareInterface; /** * ILS Driver for VuFind to query availability information via DAIA. * - * Based on the proof-of-concept-driver by Till Kinstler, GBV. - * * @category VuFind2 * @package ILS_Drivers + * @author Jochen Lienhard <lienhard@ub.uni-freiburg.de> * @author Oliver Goldschmidt <o.goldschmidt@tu-harburg.de> + * @author André Lahmann <lahmann@ub.uni-leipzig.de> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki */ -class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface +class DAIA extends AbstractBase implements + HttpServiceAwareInterface, LoggerAwareInterface { + use \VuFindHttp\HttpServiceAwareTrait; use \VuFind\Log\LoggerAwareTrait; /** - * Base URL + * Base URL for DAIA Service + * + * @var string + */ + protected $baseUrl; + + /** + * DAIA query identifier prefix * * @var string */ - protected $baseURL; + protected $daiaIdPrefix; + + /** + * DAIA response format + * + * @var string + */ + protected $daiaResponseFormat; + + /** + * DAIA legacySupport flag + * + * @var boolean + */ + protected $legacySupport = false; /** * Initialize the driver. @@ -63,11 +91,45 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface */ public function init() { - if (!isset($this->config['Global']['baseUrl'])) { - throw new ILSException('Global/baseUrl configuration needs to be set.'); + // DAIA.ini sections changed, therefore move old [Global] section to + // new [DAIA] section as fallback + if (isset($this->config['Global']) && !isset($this->config['DAIA'])) { + $this->config['DAIA'] = $this->config['Global']; + $this->legacySupport = true; + } + + if (isset($this->config['DAIA']['baseUrl'])) { + $this->baseUrl = $this->config['DAIA']['baseUrl']; + } else { + throw new ILSException('DAIA/baseUrl configuration needs to be set.'); + } + if (isset($this->config['DAIA']['daiaResponseFormat'])) { + $this->daiaResponseFormat = strtolower( + $this->config['DAIA']['daiaResponseFormat'] + ); + } else { + $this->debug("No daiaResponseFormat setting found, using default: xml"); + $this->daiaResponseFormat = "xml"; } + if (isset($this->config['DAIA']['daiaIdPrefix'])) { + $this->daiaIdPrefix = $this->config['DAIA']['daiaIdPrefix']; + } else { + $this->debug("No daiaIdPrefix setting found, using default: ppn:"); + $this->daiaIdPrefix = "ppn:"; + } + } - $this->baseURL = $this->config['Global']['baseUrl']; + /** + * Public Function which retrieves renew, hold and cancel settings from the + * driver ini file. + * + * @param string $function The name of the feature to be checked + * + * @return array An array with key-value pairs. + */ + public function getConfig($function) + { + return isset($this->config[$function]) ? $this->config[$function] : false; } /** @@ -103,8 +165,13 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface */ public function getStatus($id) { - $holding = $this->daiaToHolding($id); - return $holding; + if ($this->daiaResponseFormat == 'xml') { + return $this->getXMLStatus($id); + } elseif ($this->daiaResponseFormat == 'json') { + return $this->getJSONStatus($id); + } else { + throw new ILSException('No matching format found for status retrieval.'); + } } /** @@ -121,23 +188,20 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface public function getStatuses($ids) { $items = []; - foreach ($ids as $id) { - $items[] = $this->getShortStatus($id); + + if ($this->daiaResponseFormat == 'xml') { + foreach ($ids as $id) { + $items[] = $this->getXMLShortStatus($id); + } + } elseif ($this->daiaResponseFormat == 'json') { + foreach ($ids as $id) { + $items[] = $this->getJSONStatus($id); + } + } else { + throw new ILSException('No matching format found for status retrieval.'); } - return $items; - } - /** - * Public Function which retrieves renew, hold and cancel settings from the - * driver ini file. - * - * @param string $function The name of the feature to be checked - * - * @return array An array with key-value pairs. - */ - public function getConfig($function) - { - return isset($this->config[$function]) ? $this->config[$function] : false; + return $items; } /** @@ -177,21 +241,190 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface } /** - * Query a DAIA server and return the result as DOMDocument object. - * The returned object is an XML document containing - * content as described in the DAIA format specification. + * Perform an HTTP request. * - * @param string $id Document to look up. + * @param string $id id for query in daia * - * @return DOMDocument Object representation of an XML document containing - * content as described in the DAIA format specification. + * @return xml or json object + * @throws ILSException */ - protected function queryDAIA($id) + protected function doHTTPRequest($id) { - $daia = new DOMDocument(); - $daia->load($this->baseURL . $id); + $contentTypes = array ( + "xml" => "application/xml", + "json" => "application/json", + ); + + $http_headers = array( + "Content-type: " . $contentTypes[$this->daiaResponseFormat], + "Accept: " . $contentTypes[$this->daiaResponseFormat] + ); + + $params = array( + "id" => $this->daiaIdPrefix . $id, + "format" => $this->daiaResponseFormat, + ); + + try { + if ($this->legacySupport) { + // HttpRequest for DAIA legacy support as all + // the parameters are contained in the baseUrl + $result = $this->httpService->get( + $this->baseUrl . $id, + array(), null, $http_headers + ); + } else { + $result = $this->httpService->get( + $this->baseUrl, + $params, null, $http_headers + ); + } + } catch (\Exception $e) { + throw new ILSException($e->getMessage()); + } - return $daia; + if (!$result->isSuccess()) { + // throw ILSException disabled as this will be shown in VuFind-Frontend + //throw new ILSException('HTTP error ' . $result->getStatusCode() . + // ' retrieving status for record: ' . $id); + // write to Debug instead + $this->debug( + 'HTTP status ' . $result->getStatusCode() . + ' received, retrieving availability information for record: ' . $id + ); + + // return false as DAIA request failed + return false; + } + return ($result->getBody()); + + } + + /** + * Get Status of JSON Result + * + * This method gets a json result from the DAIA server and + * analyses it. Than a vufind result is build. + * + * @param string $id The id of the bib record + * + * @return array() of items + */ + protected function getJSONStatus($id) + { + // get daia json request for id and decode it + $daia=json_decode($this->doHTTPRequest($id), true); + $result = array(); + if (array_key_exists("message", $daia)) { + // analyse the message for the error handling and debugging + } + if (array_key_exists("instituion", $daia)) { + // information about the institution that grants or + // knows about services and their availability + // this fields could be analyzed: href, content, id + } + if (array_key_exists("document", $daia)) { + // analyse the items + $dummy_item = array("id"=>"0815", + "availability"=>true, + "status"=>"Available", + "location"=>"physical location no HTML", + "reserve"=>"N", + "callnumber"=>"007", + "number"=>"1", + "item_id"=>"0815", + "barcode"=>"1"); + // each document may contain: id, href, message, item + foreach ($daia["document"] as $document) { + $doc_id=null; + $doc_href=null; + $doc_message=null; + if (array_key_exists("id", $document)) { + $doc_id=$document["id"]; + } + if (array_key_exists("href", $document)) { + // url of the document + $doc_href=$document["href"]; + } + if (array_key_exists("message", $document)) { + // array of messages with language code and content + $doc_message=$document["message"]; + } + // if one or more items exist, iterate and build result-item + if (array_key_exists("item", $document)) { + $number=0; + foreach ($document["item"] as $item) { + $result_item=array(); + $result_item["id"]=$id; + $result_item["item_id"]=$id; + $number++; // count items + $result_item["number"]=$number; + // set default value for barcode + $result_item["barcode"]="1"; + // set default value for reserve + $result_item["reserve"]="N"; + // get callnumber + if (isset($item["label"])) { + $result_item["callnumber"]=$item["label"]; + } else { + $result_item["callnumber"]="Unknown"; + } + // get location + if (isset($item["storage"])) { + $result_item["location"]=$item["storage"]["content"]; + } else { + $result_item["location"]="Unknown"; + } + // status and availability will be calculated in own function + $result_item=$this->calculateStatus($item)+$result_item; + // add result_item to the result array + $result[]=$result_item; + } // end iteration on item + } + } // end iteration on document + // $result[]=$dummy_item; + } + return $result; + } + + /** + * Calaculate Status and Availability of an item + * + * If availability is false the string of status will be shown in vufind + * + * @param string $item json DAIA item + * + * @return array("status"=>"only for VIPs" ... ) + */ + protected function calculateStatus($item) + { + $availability = false; + $status = null; + $duedate = null; + if (array_key_exists("available", $item)) { + // check if item is loanable or presentation + foreach ($item["available"] as $available) { + if ($available["service"] == "loan") { + $availability = true; + } + if ($available["service"] == "presentation") { + $availability = true; + } + } + } + if (array_key_exists("unavailable", $item)) { + foreach ($item["unavailable"] as $unavailable) { + if ($unavailable["service"] == "loan") { + if (isset($unavailable["expected"])) { + $duedate = $unavailable["expected"]; + } + $status = "dummy text"; + } + } + } + return (array("status" => $status, + "availability" => $availability, + "duedate" => $duedate)); } /** @@ -201,11 +434,23 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface * * @return array */ - protected function daiaToHolding($id) + protected function getXMLStatus($id) { - $daia = $this->queryDAIA($id); + $daia = new DOMDocument(); + $response = $this->doHTTPRequest($id); + if ($response) { + $daia->loadXML($response); + } // get Availability information from DAIA $documentlist = $daia->getElementsByTagName('document'); + + // handle empty DAIA response + if ($documentlist->length == 0 + && $daia->getElementsByTagName("message") != null + ) { + // analyse the message for the error handling and debugging + } + $status = []; for ($b = 0; $documentlist->item($b) !== null; $b++) { $itemlist = $documentlist->item($b)->getElementsByTagName('item'); @@ -247,10 +492,12 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface 'location.id' => '', 'location.href' => '', 'label' => '', - 'notes' => [] + 'notes' => [], ]; - $result['item_id'] = $itemlist->item($c)->attributes - ->getNamedItem('id')->nodeValue; + if ($itemlist->item($c)->attributes->getNamedItem('id') !== null) { + $result['item_id'] = $itemlist->item($c)->attributes + ->getNamedItem('id')->nodeValue; + } if ($itemlist->item($c)->attributes->getNamedItem('href') !== null) { $result['recallhref'] = $itemlist->item($c)->attributes ->getNamedItem('href')->nodeValue; @@ -274,8 +521,13 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface $result['location'] = $storageElements->item(0)->nodeValue; //$result['location.id'] = $storageElements->item(0) // ->attributes->getNamedItem('id')->nodeValue; - $result['location.href'] = $storageElements->item(0) - ->attributes->getNamedItem('href')->nodeValue; + $href = $storageElements->item(0)->attributes + ->getNamedItem('href'); + if ($href !== null) { + //href attribute is recommended but not mandatory + $result['location.href'] = $storageElements->item(0) + ->attributes->getNamedItem('href')->nodeValue; + } //$result['barcode'] = $result['location.id']; } } @@ -322,53 +574,59 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface if ($unavailableElements->item(0) !== null) { for ($n = 0; $unavailableElements->item($n) !== null; $n++) { $service = $unavailableElements->item($n)->attributes - ->getNamedItem('service')->nodeValue; + ->getNamedItem('service'); $expectedNode = $unavailableElements->item($n)->attributes ->getNamedItem('expected'); $queueNode = $unavailableElements->item($n)->attributes ->getNamedItem('queue'); - if ($service === 'presentation') { - $result['presentation.availability'] = '0'; - $result['presentation_availability'] = '0'; - if ($expectedNode !== null) { - $result['presentation.duedate'] - = $expectedNode->nodeValue; - } - if ($queueNode !== null) { - $result['presentation.queue'] - = $queueNode->nodeValue; - } - $result['availability'] = '0'; - } elseif ($service === 'loan') { - $result['loan.availability'] = '0'; - $result['loan_availability'] = '0'; - if ($expectedNode !== null) { - $result['loan.duedate'] = $expectedNode->nodeValue; - } - if ($queueNode !== null) { - $result['loan.queue'] = $queueNode->nodeValue; - } - $result['availability'] = '0'; - } elseif ($service === 'interloan') { - $result['interloan.availability'] = '0'; - if ($expectedNode !== null) { - $result['interloan.duedate'] - = $expectedNode->nodeValue; - } - if ($queueNode !== null) { - $result['interloan.queue'] = $queueNode->nodeValue; - } - $result['availability'] = '0'; - } elseif ($service === 'openaccess') { - $result['openaccess.availability'] = '0'; - if ($expectedNode !== null) { - $result['openaccess.duedate'] - = $expectedNode->nodeValue; + if ($service !== null) { + $service = $service->nodeValue; + if ($service === 'presentation') { + $result['presentation.availability'] = '0'; + $result['presentation_availability'] = '0'; + if ($expectedNode !== null) { + $result['presentation.duedate'] + = $expectedNode->nodeValue; + } + if ($queueNode !== null) { + $result['presentation.queue'] + = $queueNode->nodeValue; + } + $result['availability'] = '0'; + } elseif ($service === 'loan') { + $result['loan.availability'] = '0'; + $result['loan_availability'] = '0'; + if ($expectedNode !== null) { + $result['loan.duedate'] + = $expectedNode->nodeValue; + } + if ($queueNode !== null) { + $result['loan.queue'] = $queueNode->nodeValue; + } + $result['availability'] = '0'; + } elseif ($service === 'interloan') { + $result['interloan.availability'] = '0'; + if ($expectedNode !== null) { + $result['interloan.duedate'] + = $expectedNode->nodeValue; + } + if ($queueNode !== null) { + $result['interloan.queue'] + = $queueNode->nodeValue; + } + $result['availability'] = '0'; + } elseif ($service === 'openaccess') { + $result['openaccess.availability'] = '0'; + if ($expectedNode !== null) { + $result['openaccess.duedate'] + = $expectedNode->nodeValue; + } + if ($queueNode !== null) { + $result['openaccess.queue'] + = $queueNode->nodeValue; + } + $result['availability'] = '0'; } - if ($queueNode !== null) { - $result['openaccess.queue'] = $queueNode->nodeValue; - } - $result['availability'] = '0'; } // TODO: message/limitation if ($expectedNode !== null) { @@ -385,36 +643,41 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface if ($availableElements->item(0) !== null) { for ($n = 0; $availableElements->item($n) !== null; $n++) { $service = $availableElements->item($n)->attributes - ->getNamedItem('service')->nodeValue; + ->getNamedItem('service'); $delayNode = $availableElements->item($n)->attributes ->getNamedItem('delay'); - if ($service === 'presentation') { - $result['presentation.availability'] = '1'; - $result['presentation_availability'] = '1'; - if ($delayNode !== null) { - $result['presentation.delay'] - = $delayNode->nodeValue; - } - $result['availability'] = '1'; - } elseif ($service === 'loan') { - $result['loan.availability'] = '1'; - $result['loan_availability'] = '1'; - if ($delayNode !== null) { - $result['loan.delay'] = $delayNode->nodeValue; - } - $result['availability'] = '1'; - } elseif ($service === 'interloan') { - $result['interloan.availability'] = '1'; - if ($delayNode !== null) { - $result['interloan.delay'] = $delayNode->nodeValue; - } - $result['availability'] = '1'; - } elseif ($service === 'openaccess') { - $result['openaccess.availability'] = '1'; - if ($delayNode !== null) { - $result['openaccess.delay'] = $delayNode->nodeValue; + if ($service !== null) { + $service = $service->nodeValue; + if ($service === 'presentation') { + $result['presentation.availability'] = '1'; + $result['presentation_availability'] = '1'; + if ($delayNode !== null) { + $result['presentation.delay'] + = $delayNode->nodeValue; + } + $result['availability'] = '1'; + } elseif ($service === 'loan') { + $result['loan.availability'] = '1'; + $result['loan_availability'] = '1'; + if ($delayNode !== null) { + $result['loan.delay'] = $delayNode->nodeValue; + } + $result['availability'] = '1'; + } elseif ($service === 'interloan') { + $result['interloan.availability'] = '1'; + if ($delayNode !== null) { + $result['interloan.delay'] + = $delayNode->nodeValue; + } + $result['availability'] = '1'; + } elseif ($service === 'openaccess') { + $result['openaccess.availability'] = '1'; + if ($delayNode !== null) { + $result['openaccess.delay'] + = $delayNode->nodeValue; + } + $result['availability'] = '1'; } - $result['availability'] = '1'; } // TODO: message/limitation if ($delayNode !== null) { @@ -459,9 +722,13 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface * id, availability (boolean), status, location, reserve, callnumber, duedate, * number */ - public function getShortStatus($id) + public function getXMLShortStatus($id) { - $daia = $this->queryDAIA($id); + $daia = new DOMDocument(); + $response = $this->doHTTPRequest($id); + if ($response) { + $daia->loadXML($response); + } // get Availability information from DAIA $itemlist = $daia->getElementsByTagName('item'); $label = "Unknown"; @@ -471,7 +738,7 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface for ($c = 0; $itemlist->item($c) !== null; $c++) { $earliest_href = ''; $storageElements = $itemlist->item($c)->getElementsByTagName('storage'); - if ($storageElements->item(0)->nodeValue) { + if ($storageElements->item(0) && $storageElements->item(0)->nodeValue) { if ($storageElements->item(0)->nodeValue === 'Internet') { $href = $storageElements->item(0)->attributes ->getNamedItem('href')->nodeValue; @@ -547,8 +814,10 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface foreach ($earliest as $earliest_key => $earliest_value) { if ($earliest_counter === 0) { $earliest_duedate = $earliest_value; - $earliest_href = $hrefs[$earliest_key]; - $earliest_queue = $queue[$earliest_key]; + $earliest_href = isset($hrefs[$earliest_key]) + ? $hrefs[$earliest_key] : ''; + $earliest_queue = isset($queue[$earliest_key]) + ? $queue[$earliest_key] : ''; } $earliest_counter = 1; } @@ -564,28 +833,30 @@ class DAIA extends AbstractBase implements \Zend\Log\LoggerAwareInterface $status = 'missing'; } } - if (!$status) { + if (!isset($status)) { $status = 'Unavailable'; } $availability = 0; } $reserve = 'N'; - if ($earliest_queue > 0) { + if (isset($earliest_queue) && $earliest_queue > 0) { $reserve = 'Y'; } - $holding[] = ['availability' => $availability, - 'id' => $id, - 'status' => "$status", - 'location' => "$storage", - 'reserve' => $reserve, - 'queue' => $earliest_queue, - 'callnumber' => "$label", - 'duedate' => $earliest_duedate, - 'leanable' => $leanable, - 'recallhref' => $earliest_href, - 'number' => ($c+1), - 'presenceOnly' => $presenceOnly]; + $holding[] = [ + 'availability' => $availability, + 'id' => $id, + 'status' => isset($status) ? "$status" : '', + 'location' => isset($storage) ? "$storage" : '', + 'reserve' => isset($reserve) ? $reserve : '', + 'queue' => isset($earliest_queue) ? $earliest_queue : '', + 'callnumber' => isset($label) ? "$label" : '', + 'duedate' => isset($earliest_duedate) ? $earliest_duedate : '', + 'leanable' => isset($leanable) ? $leanable : '', + 'recallhref' => isset($earliest_href) ? $earliest_href : '', + 'number' => ($c+1), + 'presenceOnly' => isset($presenceOnly) ? $presenceOnly : '', + ]; } return $holding; } -} \ No newline at end of file +}