From 25cbd09152034700b52874e6f4180be29e386b84 Mon Sep 17 00:00:00 2001 From: Chris Hallberg <crhallberg@gmail.com> Date: Tue, 22 Jan 2019 19:10:59 +0000 Subject: [PATCH] Experimental, proof-of-concept FOLIO ILS Driver (#1169) --- config/vufind/Folio.ini | 4 + .../src/VuFind/ILS/Driver/AbstractAPI.php | 134 +++ module/VuFind/src/VuFind/ILS/Driver/Folio.php | 803 ++++++++++++++++++ .../src/VuFind/ILS/Driver/FolioFactory.php | 69 ++ .../src/VuFind/ILS/Driver/PluginManager.php | 2 + .../folio/responses/check-invalid-token.json | 5 + .../folio/responses/check-valid-token.json | 4 + .../fixtures/folio/responses/get-tokens.json | 4 + .../src/VuFindTest/ILS/Driver/FolioTest.php | 192 +++++ tests/data/folio.mrc | 1 + 10 files changed, 1218 insertions(+) create mode 100644 config/vufind/Folio.ini create mode 100644 module/VuFind/src/VuFind/ILS/Driver/AbstractAPI.php create mode 100644 module/VuFind/src/VuFind/ILS/Driver/Folio.php create mode 100644 module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php create mode 100644 module/VuFind/tests/fixtures/folio/responses/check-invalid-token.json create mode 100644 module/VuFind/tests/fixtures/folio/responses/check-valid-token.json create mode 100644 module/VuFind/tests/fixtures/folio/responses/get-tokens.json create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php create mode 100644 tests/data/folio.mrc diff --git a/config/vufind/Folio.ini b/config/vufind/Folio.ini new file mode 100644 index 00000000000..94dadbd8086 --- /dev/null +++ b/config/vufind/Folio.ini @@ -0,0 +1,4 @@ +[Folio] +base_url = https://localhost:9130 +username = diku_admin +password = admin diff --git a/module/VuFind/src/VuFind/ILS/Driver/AbstractAPI.php b/module/VuFind/src/VuFind/ILS/Driver/AbstractAPI.php new file mode 100644 index 00000000000..65811acf2c9 --- /dev/null +++ b/module/VuFind/src/VuFind/ILS/Driver/AbstractAPI.php @@ -0,0 +1,134 @@ +<?php +/** + * Abstract Driver for API-based ILS drivers + * + * PHP version 5 + * + * Copyright (C) Villanova University 2018. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind + * @package ILS_Drivers + * @author Chris Hallberg <challber@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +namespace VuFind\ILS\Driver; + +use VuFind\Exception\BadRequest as BadRequest; +use VuFind\Exception\Forbidden as Forbidden; +use VuFind\Exception\ILS as ILSException; +use VuFind\Exception\RecordMissing as RecordMissing; +use VuFindHttp\HttpServiceAwareInterface; +use Zend\Log\LoggerAwareInterface; + +/** + * Abstract Driver for API-based ILS drivers + * + * @category VuFind + * @package ILS_Drivers + * @author Chris Hallberg <challber@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +abstract class AbstractAPI extends AbstractBase implements HttpServiceAwareInterface, + LoggerAwareInterface +{ + use \VuFind\Log\LoggerAwareTrait { + logError as error; + } + use \VuFindHttp\HttpServiceAwareTrait; + + /** + * Allow default corrections to all requests + * + * @param \Zend\Http\Headers $headers the request headers + * @param array $params the parameters object + * + * @return array + */ + protected function preRequest(\Zend\Http\Headers $headers, $params) + { + return [$headers, $params]; + } + + /** + * Make requests + * + * @param string $method GET/POST/PUT/DELETE/etc + * @param string $path API path (with a leading /) + * @param array $params Parameters object to be sent as data + * @param array $headers Additional headers + * + * @return \Zend\Http\Response + */ + public function makeRequest($method = "GET", $path = "/", $params = [], + $headers = [] + ) { + $client = $this->httpService->createClient( + $this->config['API']['base_url'] . $path, + $method, + 120 + ); + // error_log($method . ' ' . $this->config['API']['base_url'] . $path); + + // Add default headers and parameters + $req_headers = $client->getRequest()->getHeaders(); + $req_headers->addHeaders($headers); + list($req_headers, $params) = $this->preRequest($req_headers, $params); + + // Add params + if ($method == 'GET') { + $client->setParameterGet($params); + } else { + if (is_string($params)) { + $client->getRequest()->setContent($params); + } else { + $client->setParameterPost($params); + } + } + $response = $client->send(); + switch ($response->getStatusCode()) { + case 400: + throw new BadRequest($response->getBody()); + case 401: + case 403: + throw new Forbidden($response->getBody()); + case 404: + throw new RecordMissing($response->getBody()); + case 500: + throw new ILSException("500: Internal Server Error"); + } + return $response; + } + + /** + * Set the configuration for the driver. + * + * @param array $config Configuration array (usually loaded from a VuFind .ini + * file whose name corresponds with the driver class name). + * + * @throws ILSException if base url excluded + * @return void + */ + public function setConfig($config) + { + parent::setConfig($config); + // Base URL required for API drivers + if (!isset($config['API']['base_url'])) { + throw new ILSException('API Driver configured without base url.'); + } + } +} diff --git a/module/VuFind/src/VuFind/ILS/Driver/Folio.php b/module/VuFind/src/VuFind/ILS/Driver/Folio.php new file mode 100644 index 00000000000..356334a88d8 --- /dev/null +++ b/module/VuFind/src/VuFind/ILS/Driver/Folio.php @@ -0,0 +1,803 @@ +<?php +/** + * FOLIO REST API driver + * + * PHP version 5 + * + * Copyright (C) Villanova University 2018. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind + * @package ILS_Drivers + * @author Chris Hallberg <challber@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +namespace VuFind\ILS\Driver; + +use VuFind\Exception\ILS as ILSException; +use VuFind\I18n\Translator\TranslatorAwareInterface; +use VuFindHttp\HttpServiceAwareInterface as HttpServiceAwareInterface; + +/** + * FOLIO REST API driver + * + * @category VuFind + * @package ILS_Drivers + * @author Chris Hallberg <challber@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +class Folio extends AbstractAPI implements + HttpServiceAwareInterface, TranslatorAwareInterface +{ + use \VuFindHttp\HttpServiceAwareTrait; + use \VuFind\I18n\Translator\TranslatorAwareTrait; + + /** + * Authentication tenant (X-Okapi-Tenant) + * + * @var string + */ + protected $tenant = null; + + /** + * Authentication token (X-Okapi-Token) + * + * @var string + */ + protected $token = null; + + /** + * Factory function for constructing the SessionContainer. + * + * @var Callable + */ + protected $sessionFactory; + + /** + * Session cache + * + * @var \Zend\Session\Container + */ + protected $sessionCache; + + /** + * Constructor + * + * @param \VuFind\Date\Converter $dateConverter Date converter object + * @param Callable $sessionFactory Factory function returning + * SessionContainer object + */ + public function __construct(\VuFind\Date\Converter $dateConverter, + $sessionFactory + ) { + $this->dateConverter = $dateConverter; + $this->sessionFactory = $sessionFactory; + } + + /** + * Set the configuration for the driver. + * + * @param array $config Configuration array (usually loaded from a VuFind .ini + * file whose name corresponds with the driver class name). + * + * @throws ILSException if base url excluded + * @return void + */ + public function setConfig($config) + { + parent::setConfig($config); + $this->tenant = $this->config['API']['tenant']; + } + + /** + * (From AbstractAPI) Allow default corrections to all requests + * + * Add X-Okapi headers and Content-Type to every request + * + * @param \Zend\Http\Headers $headers the request headers + * @param object $params the parameters object + * + * @return array + */ + public function preRequest(\Zend\Http\Headers $headers, $params) + { + $headers->addHeaderLine('Accept', 'application/json'); + if (!$headers->has('Content-Type')) { + $headers->addHeaderLine('Content-Type', 'application/json'); + } + $headers->addHeaderLine('X-Okapi-Tenant', $this->tenant); + if ($this->token != null) { + $headers->addHeaderLine('X-Okapi-Token', $this->token); + } + return [$headers, $params]; + } + + /** + * Login and receive a new token + * + * @return void + */ + protected function renewTenantToken() + { + $this->token = null; + $auth = [ + 'username' => $this->config['API']['username'], + 'password' => $this->config['API']['password'], + ]; + $response = $this->makeRequest('POST', '/authn/login', json_encode($auth)); + if ($response->getStatusCode() >= 400) { + throw new ILSException($response->getBody()); + } + $this->token = $response->getHeaders()->get('X-Okapi-Token') + ->getFieldValue(); + $this->sessionCache->folio_token = $this->token; + } + + /** + * Check if our token is still valid + * + * Method taken from Stripes JS (loginServices.js:validateUser) + * + * @return void + */ + protected function checkTenantToken() + { + $response = $this->makeRequest('GET', '/users'); + if ($response->getStatusCode() >= 400) { + $this->token = null; + $this->renewTenantToken(); + } + } + + /** + * Initialize the driver. + * + * Check or renew our auth token + * + * @return void + */ + public function init() + { + $factory = $this->sessionFactory; + $this->sessionCache = $factory($this->tenant); + if ($this->sessionCache->folio_token ?? false) { + $this->token = $this->sessionCache->folio_token; + } + if ($this->token == null) { + $this->renewTenantToken(); + } else { + $this->checkTenantToken(); + } + } + + /** + * Get local bib id from inventory by following parents up the tree + * + * @param string $instanceId Instance-level id (lowest level) + * @param string $holdingId Holding-level id (looked up from instance if null) + * @param string $itemId Item-level id (looked up from holding if null) + * + * @return string Local bib id retrieved from Folio identifiers + */ + protected function getBibId($instanceId, $holdingId = null, $itemId = null) + { + if ($instanceId == null) { + if ($holdingId == null) { + $response = $this->makeRequest( + 'GET', + '/item-storage/items/' . $itemId + ); + $item = json_decode($response->getBody()); + $holdingId = $item->holdingsRecordId; + } + $response = $this->makeRequest( + 'GET', '/holdings-storage/holdings/' . $holdingId + ); + $holding = json_decode($response->getBody()); + $instanceId = $holding->instanceId; + } + $response = $this->makeRequest( + 'GET', '/inventory/instances/' . $instanceId + ); + $instance = json_decode($response->getBody()); + return $instance->identifiers[0]->value; + } + + /** + * Get raw object of item from inventory/items/ + * + * @param string $bibId Bib-level id + * + * @throw + * @return array + */ + protected function getInstance($bibId) + { + $escaped = str_replace('"', '\"', str_replace('&', '%26', $bibId)); + $query = [ + 'query' => '(id="' . $escaped . '" or identifiers="' . $escaped . '")' + ]; + $response = $this->makeRequest('GET', '/instance-storage/instances', $query); + $instances = json_decode($response->getBody()); + if (count($instances->instances) == 0) { + throw new ILSException("Item Not Found"); + } + return $instances->instances[0]; + } + + /** + * Get raw object of item from inventory/items/ + * + * @param string $itemId Item-level id + * + * @return array + */ + public function getStatus($itemId) + { + return $this->getHolding($itemId); + } + + /** + * This method calls getStatus for an array of records or implement a bulk method + * + * @param array $idList Item-level ids + * + * @return array values from getStatus + */ + public function getStatuses($idList) + { + $status = []; + foreach ($idList as $id) { + $status[] = $this->getStatus($id); + } + return $status; + } + + /** + * Retrieves renew, hold and cancel settings from the driver ini file. + * + * @param string $function The name of the feature to be checked + * @param array $params Optional feature-specific parameters (array) + * + * @return array An array with key-value pairs. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getConfig($function, $params = null) + { + return $this->config[$function] ?? false; + } + + /** + * This method queries the ILS for holding information. + * + * @param string $bibId Bib-level id + * @param array $patron Patron login information from $this->patronLogin + * + * @return array An array of associative holding arrays + */ + public function getHolding($bibId, array $patron = null) + { + $instance = $this->getInstance($bibId); + $query = ['query' => '(instanceId="' . $instance->id . '")']; + $holdingResponse = $this->makeRequest( + 'GET', + '/holdings-storage/holdings', + $query + ); + $holdingBody = json_decode($holdingResponse->getBody()); + $items = []; + for ($i = 0; $i < count($holdingBody->holdingsRecords); $i++) { + $holding = $holdingBody->holdingsRecords[$i]; + $locationName = ''; + if (!empty($holding->permanentLocationId)) { + $locationResponse = $this->makeRequest( + 'GET', + '/locations/' . $holding->permanentLocationId + ); + $location = json_decode($locationResponse->getBody()); + $locationName = $location->name; + } + + $query = ['query' => '(holdingsRecordId="' . $holding->id . '")']; + $itemResponse = $this->makeRequest('GET', '/item-storage/items', $query); + $itemBody = json_decode($itemResponse->getBody()); + for ($j = 0; $j < count($itemBody->items); $j++) { + $item = $itemBody->items[$j]; + $items[] = [ + 'id' => $bibId, + 'item_id' => $instance->id, + 'holding_id' => $holding->id, + 'number' => count($items), + 'barcode' => $item->barcode ?? '', + 'status' => $item->status->name, + 'availability' => $item->status->name == 'Available', + 'notes' => $item->notes ?? [], + 'callnumber' => $holding->callNumber, + 'location' => $locationName, + 'reserve' => 'TODO', + 'addLink' => true + ]; + } + } + return $items; + } + + /** + * Patron Login + * + * This is responsible for authenticating a patron against the catalog. + * + * @param string $username The patron username + * @param string $password The patron password + * + * @return mixed Associative array of patron info on successful login, + * null on unsuccessful login. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function patronLogin($username, $password) + { + // Get user id + $query = ['query' => 'username == ' . $username]; + $response = $this->makeRequest('GET', '/users', $query); + $json = json_decode($response->getBody()); + if (count($json->users) == 0) { + throw new ILSException("User not found"); + } + $profile = $json->users[0]; + $credentials = [ + 'userId' => $profile->id, + 'username' => $username, + 'password' => $password, + ]; + // Get token + try { + $response = $this->makeRequest( + 'POST', + '/authn/login', + json_encode($credentials) + ); + // Replace admin with user as tenant + $this->token = $response->getHeaders()->get('X-Okapi-Token') + ->getFieldValue(); + return [ + 'id' => $profile->id, + 'username' => $username, + 'cat_username' => $username, + 'cat_password' => $password, + 'firstname' => $profile->personal->firstName ?? null, + 'lastname' => $profile->personal->lastName ?? null, + 'email' => $profile->personal->email ?? null, + ]; + } catch (Exception $e) { + return null; + } + } + + /** + * This method queries the ILS for a patron's current profile information + * + * @param array $patron Patron login information from $this->patronLogin + * + * @return array Profile data in associative array + */ + public function getMyProfile($patron) + { + $query = ['query' => 'username == "' . $patron['username'] . '"']; + $response = $this->makeRequest('GET', '/users', $query); + $users = json_decode($response->getBody()); + $profile = $users->users[0]; + return [ + 'id' => $profile->id, + 'firstname' => $profile->personal->firstName ?? null, + 'lastname' => $profile->personal->lastName ?? null, + 'address1' => $profile->personal->addresses[0]->addressLine1 ?? null, + 'city' => $profile->personal->addresses[0]->city ?? null, + 'country' => $profile->personal->addresses[0]->countryId ?? null, + 'zip' => $profile->personal->addresses[0]->postalCode ?? null, + 'phone' => $profile->personal->phone ?? null, + 'mobile_phone' => $profile->personal->mobilePhone ?? null, + 'expiration_date' => $profile->expirationDate ?? null, + ]; + } + + /** + * This method queries the ILS for a patron's current checked out items + * + * Input: Patron array returned by patronLogin method + * Output: Returns an array of associative arrays. + * Each associative array contains these keys: + * duedate - The item's due date (a string). + * dueTime - The item's due time (a string, optional). + * dueStatus - A special status – may be 'due' (for items due very soon) + * or 'overdue' (for overdue items). (optional). + * id - The bibliographic ID of the checked out item. + * source - The search backend from which the record may be retrieved + * (optional - defaults to Solr). Introduced in VuFind 2.4. + * barcode - The barcode of the item (optional). + * renew - The number of times the item has been renewed (optional). + * renewLimit - The maximum number of renewals allowed + * (optional - introduced in VuFind 2.3). + * request - The number of pending requests for the item (optional). + * volume – The volume number of the item (optional). + * publication_year – The publication year of the item (optional). + * renewable – Whether or not an item is renewable + * (required for renewals). + * message – A message regarding the item (optional). + * title - The title of the item (optional – only used if the record + * cannot be found in VuFind's index). + * item_id - this is used to match up renew responses and must match + * the item_id in the renew response. + * institution_name - Display name of the institution that owns the item. + * isbn - An ISBN for use in cover image loading + * (optional – introduced in release 2.3) + * issn - An ISSN for use in cover image loading + * (optional – introduced in release 2.3) + * oclc - An OCLC number for use in cover image loading + * (optional – introduced in release 2.3) + * upc - A UPC for use in cover image loading + * (optional – introduced in release 2.3) + * borrowingLocation - A string describing the location where the item + * was checked out (optional – introduced in release 2.4) + * + * @param array $patron Patron login information from $this->patronLogin + * + * @return array Transactions associative arrays + */ + public function getMyTransactions($patron) + { + $query = ['query' => 'userId==' . $patron['username']]; + $response = $this->makeRequest("GET", '/circulation/loans', $query); + $json = json_decode($response->getBody()); + if (count($json->loans) == 0) { + return []; + } + $transactions = []; + foreach ($json->loans as $trans) { + $dueDate = date_create($trans['dueDate']); + $transactions[] = [ + 'duedate' => date_format($date, "j M Y"), + 'dueTime' => date_format($date, "g:i:s a"), + // TODO: Due Status + // 'dueStatus' => $trans['itemId'], + 'id' => $trans['itemId'], + 'barcode' => $trans['item']['barcode'], + 'title' => $trans['item']['title'], + ]; + } + return $transactions; + } + + /** + * Get Pick Up Locations + * + * This is responsible get a list of valid locations for holds / recall + * retrieval + * + * @param array $patron Patron information returned by $this->patronLogin + * + * @return array An array of associative arrays with locationID and + * locationDisplay keys + */ + public function getPickupLocations($patron) + { + $response = $this->makeRequest('GET', '/locations'); + $json = json_decode($response->getBody()); + $locations = []; + foreach ($json->locations as $location) { + $locations[] = [ + 'locationID' => $location->id, + 'locationDisplay' => $location->name + ]; + } + return $locations; + } + + /** + * This method queries the ILS for a patron's current holds + * + * Input: Patron array returned by patronLogin method + * Output: Returns an array of associative arrays, one for each hold associated + * with the specified account. Each associative array contains these keys: + * type - A string describing the type of hold – i.e. hold vs. recall + * (optional). + * id - The bibliographic record ID associated with the hold (optional). + * source - The search backend from which the record may be retrieved + * (optional - defaults to Solr). Introduced in VuFind 2.4. + * location - A string describing the pickup location for the held item + * (optional). In VuFind 1.2, this should correspond with a locationID value from + * getPickUpLocations. In VuFind 1.3 and later, it may be either + * a locationID value or a raw ready-to-display string. + * reqnum - A control number for the request (optional). + * expire - The expiration date of the hold (a string). + * create - The creation date of the hold (a string). + * position – The position of the user in the holds queue (optional) + * available – Whether or not the hold is available (true/false) (optional) + * item_id – The item id the request item (optional). + * volume – The volume number of the item (optional) + * publication_year – The publication year of the item (optional) + * title - The title of the item + * (optional – only used if the record cannot be found in VuFind's index). + * isbn - An ISBN for use in cover image loading (optional) + * issn - An ISSN for use in cover image loading (optional) + * oclc - An OCLC number for use in cover image loading (optional) + * upc - A UPC for use in cover image loading (optional) + * cancel_details - The cancel token, or a blank string if cancel is illegal + * for this hold; if omitted, this will be dynamically generated using + * getCancelHoldDetails(). You should only fill this in if it is more efficient + * to calculate the value up front; if it is an expensive calculation, you should + * omit the value entirely and let getCancelHoldDetails() do its job on demand. + * This optional feature was introduced in release 3.1. + * + * @param array $patron Patron login information from $this->patronLogin + * + * @return array Associative array of holds information + */ + public function getMyHolds($patron) + { + // Get user id + $query = ['query' => 'username == "' . $patron['username'] . '"']; + $response = $this->makeRequest('GET', '/users', $query); + $users = json_decode($response->getBody()); + $query = [ + 'query' => 'requesterId == "' . $users->users[0]->id . '"' . + ' and requestType == "Hold"' + ]; + // Request HOLDS + $response = $this->makeRequest('GET', '/request-storage/requests', $query); + $json = json_decode($response->getBody()); + $holds = []; + foreach ($json->requests as $hold) { + $holds[] = [ + 'type' => 'Hold', + 'create' => $hold->requestDate, + 'expire' => $hold->requestExpirationDate, + 'id' => $this->getBibId(null, null, $hold->itemId), + ]; + } + return $holds; + } + + /** + * Place Hold + * + * Attempts to place a hold or recall on a particular item and returns + * an array with result details. + * + * @param array $holdDetails An array of item and patron data + * + * @return mixed An array of data on the request including + * whether or not it was successful and a system message (if available) + */ + public function placeHold($holdDetails) + { + try { + $requiredBy = date_create_from_format( + 'm-d-Y', + $holdDetails['requiredBy'] + ); + } catch (Exception $e) { + throw new ILSException('hold_date_invalid'); + } + // Get status to separate Holds (on checked out items) and Pages on a + $status = $this->getStatus($holdDetails['item_id']); + $requestBody = [ + 'requestType' => $item->status->name == 'Available' ? 'Page' : 'Hold', + 'requestDate' => date('c'), + 'requesterId' => $holdDetails['patron']['id'], + 'requester' => [ + 'firstName' => $holdDetails['patron']['firstname'] ?? '', + 'lastName' => $holdDetails['patron']['lastname'] ?? '' + ], + 'itemId' => $holdDetails['item_id'], + 'fulfilmentPreference' => 'Hold Shelf', + 'requestExpirationDate' => date_format($requiredBy, 'Y-m-d'), + ]; + $response = $this->makeRequest( + 'POST', + '/request-storage/requests', + json_encode($requestBody) + ); + if ($response->isSuccess()) { + return [ + 'success' => true, + 'status' => $response->getBody() + ]; + } else { + throw new ILSException($response->getBody()); + } + } + + // @codingStandardsIgnoreStart + /** NOT FINISHED BELOW THIS LINE **/ + + /** + * Check for request blocks. + * + * @param array $patron The patron array with username and password + * + * @return array|boolean An array of block messages or false if there are no + * blocks + * @author Michael Birkner + */ + public function getRequestBlocks($patron) + { + return false; + } + + /** + * This method returns information on recently received issues of a serial. + * + * Input: Bibliographic record ID + * Output: Array of associative arrays, each with a single key: + * issue - String describing the issue + * + * Currently, most drivers do not implement this method, instead always returning + * an empty array. It is only necessary to implement this in more detail if you + * want to populate the “Most Recent Received Issues†section of the record + * holdings tab. + */ + public function getPurchaseHistory($bibID) + { + return []; + } + + /** + * This method returns items that are on reserve for the specified course, + * instructor and/or department. + * + * Input: CourseID, InstructorID, DepartmentID (these values come from the + * corresponding getCourses, getInstructors and getDepartments methods; any of + * these three filters may be set to a blank string to skip) + * Output: An array of associative arrays representing reserve items. Keys: + * BIB_ID - The record ID of the current reserve item. + * COURSE_ID - The course ID associated with the + * current reserve item, if any (required when using Solr-based reserves). + * DEPARTMENT_ID - The department ID associated with the current + * reserve item, if any (required when using Solr-based reserves). + * INSTRUCTOR_ID - The instructor ID associated with the current + * reserve item, if any (required when using Solr-based reserves). + * + */ + public function findReserves($courseID, $instructorID, $departmentID) + { + } + + /** + * This method queries the ILS for a patron's current fines + * + * Input: Patron array returned by patronLogin method + * Output: Returns an array of associative arrays, one for each fine + * associated with the specified account. Each associative array contains + * these keys: + * amount - The total amount of the fine IN PENNIES. Be sure to adjust + * decimal points appropriately (i.e. for a $1.00 fine, amount should be 100). + * checkout - A string representing the date when the item was + * checked out. + * fine - A string describing the reason for the fine + * (i.e. “Overdueâ€, “Long Overdueâ€). + * balance - The unpaid portion of the fine IN PENNIES. + * createdate – A string representing the date when the fine was accrued + * (optional) + * duedate - A string representing the date when the item was due. + * id - The bibliographic ID of the record involved in the fine. + * source - The search backend from which the record may be retrieved + * (optional - defaults to Solr). Introduced in VuFind 2.4. + * + */ + public function getMyFines($patron) + { + return []; + } + + /** + * Get a list of funds that can be used to limit the “new item†search. Note that + * “fund†may be a misnomer – if funds are not an appropriate way to limit your + * new item results, you can return a different set of values from this function. + * For example, you might just make this a wrapper for getDepartments(). The + * important thing is that whatever you return from this function, the IDs can be + * used as a limiter to the getNewItems() function, and the names are appropriate + * for display on the new item search screen. If you do not want or support such + * limits, just return an empty array here and the limit control on the new item + * search screen will disappear. + * + * Output: An associative array with key = fund ID, value = fund name. + * + * IMPORTANT: The return value for this method changed in r2184. If you are using + * VuFind 1.0RC2 or earlier, this function returns a flat array of options + * (no ID-based keys), and empty return values may cause problems. It is + * recommended that you update to newer code before implementing the new item + * feature in your driver. + */ + public function getFunds() + { + return []; + } + + /** + * This method retrieves a patron's historic transactions + * (previously checked out items). + * + * :!: The getConfig method must return a non-false value for this feature to be + * enabled. For privacy reasons, the entire feature should be disabled by default + * unless explicitly turned on in the driver's .ini file. + * + * This feature was added in VuFind 5.0. + * + * getConfig may return the following keys if the service supports paging on + * the ILS side: + * max_results - Maximum number of results that can be requested at once. + * Overrides the config.ini Catalog section setting historic_loan_page_size. + * page_size - An array of allowed page sizes + * (number of records per page) + * default_page_size - Default number of records per page + * getConfig may return the following keys if the service supports sorting: + * sort - An associative array where each key is a sort key and its + * value is a translation key + * default_sort - Default sort key + * Input: Patron array returned by patronLogin method and an array of + * optional parameters (keys = 'limit', 'page', 'sort'). + * Output: Returns an array of associative arrays containing some or all of + * these keys: + * title - item title + * checkoutDate - date checked out + * dueDate - date due + * id - bibliographic ID + * barcode - item barcode + * returnDate - date returned + * publication_year - publication year + * volume - item volume + * institution_name - owning institution + * borrowingLocation - checkout location + * message - message about the transaction + * + */ + public function getMyTransactionHistory($patron) + { + return[]; + } + + /** + * This method queries the ILS for new items + * + * Input: getNewItems takes the following parameters: + * page - page number of results to retrieve (counting starts at 1) + * limit - the size of each page of results to retrieve + * daysOld - the maximum age of records to retrieve in days (maximum 30) + * fundID - optional fund ID to use for limiting results (use a value + * returned by getFunds, or exclude for no limit); note that “fund†may be a + * misnomer – if funds are not an appropriate way to limit your new item results, + * you can return a different set of values from getFunds. The important thing is + * that this parameter supports an ID returned by getFunds, whatever that may + * mean. + * Output: An associative array with two keys: 'count' (the number of items + * in the 'results' array) and 'results' (an array of associative arrays, each + * with a single key: 'id', a record ID). + * + * IMPORTANT: The fundID parameter changed behavior in r2184. In VuFind 1.0RC2 + * and earlier versions, it receives one of the VALUES returned by getFunds(); + * in more recent code, it receives one of the KEYS from getFunds(). See getFunds + * for additional notes. + */ + public function getNewItems($page = 1, $limit, $daysOld = 30, $fundID = null) + { + return []; + } + + // @codingStandardsIgnoreEnd +} diff --git a/module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php b/module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php new file mode 100644 index 00000000000..a37d4b6a1a5 --- /dev/null +++ b/module/VuFind/src/VuFind/ILS/Driver/FolioFactory.php @@ -0,0 +1,69 @@ +<?php +/** + * Factory for Folio ILS driver. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2018. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package ILS_Drivers + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\ILS\Driver; + +use Interop\Container\ContainerInterface; + +/** + * Factory for Folio ILS driver. + * + * @category VuFind + * @package ILS_Drivers + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class FolioFactory extends DriverWithDateConverterFactory +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options passed to factory.'); + } + $sessionFactory = function ($namespace) use ($container) { + $manager = $container->get('Zend\Session\SessionManager'); + return new \Zend\Session\Container("Folio_$namespace", $manager); + }; + return parent::__invoke($container, $requestedName, [$sessionFactory]); + } +} diff --git a/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php b/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php index 020df9446d6..0382cdc0b01 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php +++ b/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php @@ -50,6 +50,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'daia' => 'VuFind\ILS\Driver\DAIA', 'demo' => 'VuFind\ILS\Driver\Demo', 'evergreen' => 'VuFind\ILS\Driver\Evergreen', + 'folio' => 'VuFind\ILS\Driver\Folio', 'horizon' => 'VuFind\ILS\Driver\Horizon', 'horizonxmlapi' => 'VuFind\ILS\Driver\HorizonXMLAPI', 'innovative' => 'VuFind\ILS\Driver\Innovative', @@ -86,6 +87,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'VuFind\ILS\Driver\Demo' => 'VuFind\ILS\Driver\DemoFactory', 'VuFind\ILS\Driver\Evergreen' => 'Zend\ServiceManager\Factory\InvokableFactory', + 'VuFind\ILS\Driver\Folio' => 'VuFind\ILS\Driver\FolioFactory', 'VuFind\ILS\Driver\Horizon' => 'VuFind\ILS\Driver\DriverWithDateConverterFactory', 'VuFind\ILS\Driver\HorizonXMLAPI' => diff --git a/module/VuFind/tests/fixtures/folio/responses/check-invalid-token.json b/module/VuFind/tests/fixtures/folio/responses/check-invalid-token.json new file mode 100644 index 00000000000..e2f94972372 --- /dev/null +++ b/module/VuFind/tests/fixtures/folio/responses/check-invalid-token.json @@ -0,0 +1,5 @@ +[ + { "status": 400 }, + { "headers": { "X-Okapi-Token": "x-okapi-token-after-invalid" } }, + { "body": "{ \"locations\": [] }" } +] diff --git a/module/VuFind/tests/fixtures/folio/responses/check-valid-token.json b/module/VuFind/tests/fixtures/folio/responses/check-valid-token.json new file mode 100644 index 00000000000..91a26fdc14d --- /dev/null +++ b/module/VuFind/tests/fixtures/folio/responses/check-valid-token.json @@ -0,0 +1,4 @@ +[ + { "status": 200 }, + { "body": "{ \"loans\": [] }" } +] diff --git a/module/VuFind/tests/fixtures/folio/responses/get-tokens.json b/module/VuFind/tests/fixtures/folio/responses/get-tokens.json new file mode 100644 index 00000000000..a740982265a --- /dev/null +++ b/module/VuFind/tests/fixtures/folio/responses/get-tokens.json @@ -0,0 +1,4 @@ +[ + { "headers": { "X-Okapi-Token": "x-okapi-token-config-tenant" } }, + { "body": "{ \"users\": [ { \"id\": \"id\" } ] }" } +] diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php new file mode 100644 index 00000000000..8242d4da2e9 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/FolioTest.php @@ -0,0 +1,192 @@ +<?php +/** + * ILS driver test + * + * PHP version 7 + * + * Copyright (C) Villanova University 2011. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @author Jochen Lienhard <lienhard@ub.uni-freiburg.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFindTest\ILS\Driver; + +use InvalidArgumentException; + +use VuFind\ILS\Driver\Folio; + +/** + * ILS driver test + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class FolioTest extends \VuFindTest\Unit\TestCase +{ + protected $testConfig = [ + 'API' => [ + 'base_url' => 'localhost', + 'tenant' => 'config_tenant', + 'username' => 'config_username', + 'password' => 'config_password' + ] + ]; + + protected $testResponses = null; + + protected $testRequestLog = []; + + protected $driver = null; + + /** + * Replace makeRequest to inject test returns + * + * @param string $method GET/POST/PUT/DELETE/etc + * @param string $path API path (with a leading /) + * @param array $params Parameters object to be sent as data + * @param array $headers Additional headers + * + * @return \Zend\Http\Response + */ + public function mockMakeRequest($method = "GET", $path = "/", $params = [], $headers = []) + { + // Run preRequest + $httpHeaders = new \Zend\Http\Headers(); + $httpHeaders->addHeaders($headers); + list($httpHeaders, $params) = $this->driver->preRequest($httpHeaders, $params); + // Log request + $this->testRequestLog[] = [ + 'method' => $method, + 'path' => $path, + 'params' => $params, + 'headers' => $httpHeaders->toArray() + ]; + // Create response + $testResponse = array_shift($this->testResponses); + $response = new \Zend\Http\Response(); + $response->setStatusCode($testResponse['status'] ?? 200); + $response->setContent($testResponse['body'] ?? ''); + $response->getHeaders()->addHeaders($testResponse['headers'] ?? []); + return $response; + } + + /** + * Generate a new Folio driver to return responses set in a json fixture + * + * Overwrites $this->driver + * Uses session cache + */ + protected function createConnector($test) + { + // Setup test responses + $file = realpath( + __DIR__ . + '/../../../../../../tests/fixtures/folio/responses/' . $test . '.json' + ); + if (!is_string($file) || !file_exists($file) || !is_readable($file)) { + throw new InvalidArgumentException( + sprintf('Unable to load fixture file: %s ', $file) + ); + } + $this->testResponses = json_decode(file_get_contents($file), true); + // Reset log + $this->testRequestLog = []; + // Session factory + $factory = function ($namespace) { + $manager = new \Zend\Session\SessionManager(); + return new \Zend\Session\Container("Folio_$namespace", $manager); + }; + // Create a stub for the SomeClass class + $this->driver = $this->getMockBuilder(\VuFind\ILS\Driver\Folio::class) + ->setConstructorArgs([new \VuFind\Date\Converter(), $factory]) + ->setMethods(['makeRequest']) + ->getMock(); + // Configure the stub + $this->driver->setConfig($this->testConfig); + $this->driver->expects($this->any()) + ->method('makeRequest') + ->will($this->returnCallback([$this, 'mockMakeRequest'])); + $this->driver->init(); + } + + /** + * Request a token where one does not exist + */ + public function testTokens() + { + $this->createConnector('get-tokens'); // saves to $this->driver + $profile = $this->driver->getMyProfile(['username' => 'whatever']); + // Get token + // - Right URL + $this->assertEquals('/authn/login', $this->testRequestLog[0]['path']); + // - Right tenant + $this->assertEquals( + $this->testConfig['API']['tenant'], + $this->testRequestLog[0]['headers']['X-Okapi-Tenant'] + ); + // Profile request + // - Passed correct token + $this->assertEquals( + 'x-okapi-token-config-tenant', // from fixtures: get-tokens.json + $this->testRequestLog[1]['headers']['X-Okapi-Token'] + ); + } + + /** + * Check a valid token retrieved from session cache + */ + public function testCheckValidToken() + { + $this->createConnector('check-valid-token'); + $profile = $this->driver->getMyTransactions(['username' => 'whatever']); + // Check token + $this->assertEquals('/users', $this->testRequestLog[0]['path']); + // Move to method call + $this->assertEquals('/circulation/loans', $this->testRequestLog[1]['path']); + // - Passed correct token + $this->assertEquals( + 'x-okapi-token-config-tenant', // from fixtures: get-tokens.json (cached) + $this->testRequestLog[1]['headers']['X-Okapi-Token'] + ); + } + + /** + * Check and renew an invalid token retrieved from session cache + */ + public function testCheckInvalidToken() + { + $this->createConnector('check-invalid-token'); + $profile = $this->driver->getPickupLocations(['username' => 'whatever']); + // Check token + $this->assertEquals('/users', $this->testRequestLog[0]['path']); + // Request new token + $this->assertEquals('/authn/login', $this->testRequestLog[1]['path']); + // Move to method call + $this->assertEquals('/locations', $this->testRequestLog[2]['path']); + // - Passed correct token + $this->assertEquals( + 'x-okapi-token-after-invalid', // from fixtures: check-invalid-token.json + $this->testRequestLog[2]['headers']['X-Okapi-Token'] + ); + } +} diff --git a/tests/data/folio.mrc b/tests/data/folio.mrc new file mode 100644 index 00000000000..912c5bd500b --- /dev/null +++ b/tests/data/folio.mrc @@ -0,0 +1 @@ +01518cas a22003611a 45 00100370000000500170003700800410005401000360009502200140013103200170014503500160016203700660017804001280024404900170037205000180038921000270040722200400043424500240047424600400049826000450053830000270058331000190061036200570062950000220068650000690070851500310077765000490080865000360085765000320089371000600092578000630098578501080104837b1a546-a826-450b-b57a-9476c4e42b2020091117105557.0840328d19831987nyufx1p o 0 a0eng d a 84643014 //r88zsn 84011556 0 a0748-1985 a002051bUSPS aocm10569413 bHuman Sciences Press, Inc., 72 5th Ave., New York, N.Y. 10011 aKSWcKSWdCLUdAIPdNSTdDLCdNSDdNSTdDLCdHULdNSTdNSDdNLMdNSTdNSDdNSTdNSDdDLCdNSTdNSDdNSTdCLUdNSTdOCLdPVU aPVUP[SERIAL]00aAC489.R3bJ680 aJ. ration. emot. ther. 0aJournal of rational emotive therapy00a14 cows for America13aJournal of rational-emotive therapy a[New York] :bThe Institute,c1983-1987. a5 v. :bill. ;c26 cm. aTwo no. a year0 aVol. 1, no. 1 (fall 1983)-v. 5, no. 4 (winter 1987). aTitle from cover. aVols. for <spring 1985-> published by Human Sciences Press, Inc. aVol. 1 has only one issue. 0aRational-emotive psychotherapyvPeriodicals. 0aCognitive therapyvPeriodicals. 2aPsychotherapyvperiodicals.2 aInstitute for Rational-Emotive Therapy (New York, N.Y.)00tRational livingx0034-0049w(OCLC)1763461w(DLC) 7200064700tJournal of rational-emotive and cognitive-behavior therapyx0894-9085w(DLC) 88648219w(OCoLC)1630781401234cas a22003011a 45 0010037000000050017000370070014000540080041000680100023001090220014001320300011001460350016001570400083001730500018002562100019002742220020002932450035003132600049003483000028003973620050004255500050004755500132005255800063006576500032007207100060007527100035008127850085008473e38354f-e93a-48f2-b54d-c50265b1504c20091117105643.0hd bfa b|c|751101d19661983nyufr1p 0 a0eng a 72000647 //r8520 a0034-0049 aATLVBX aocm01763461 aDLCcMULdNSDdDLCdRCSdAIPdDLCdNSDdNSTdDLCdAIPdNSTdDLCdNSTdNSDdNST00aAA790.A1bR351 aRation. living 0aRational living00a1963 Birmingham church bombing a[New York] :bInstitute for Rational Living. a18 v. :bill. ;c27 cm.0 aVol. 1 (Feb. 1966)-v.18, no. 1 (spring 1983). aJournal of the Institute for Rational Living. aIssued by: Institute for Rational Living, Feb. 1966-spring 1982; Institute for Rational-Emotive Therapy, fall 1982-spring 1983. aContinued in 1983 by: Journal of rational emotive therapy. 0aMental healthvPeriodicals.2 aInstitute for Rational-Emotive Therapy (New York, N.Y.)2 aInstitute for Rational Living.10tJournal of rational emotive therapyx0748-1985w(DLC) 84643014w(OCoLC)1056941301060cas a22003011a 45 001003700000005001700037008004100054010003000095022001400125035001600139037005600155040003300211050002100244210002900265222003800294245001700332260004800349300002800397310001400425362003300439362004000472500002200512650003200534650003500566650004200601785009900643936001600742db08002d-449c-4a21-b154-d083bd20462820091117104735.0820311d19831998nyuqr1p o 0 a0eng d a 83-644252 zsn82-2030 0 a0731-7158 aocm08235783 bHaworth Press, 28 E. 22nd St., New York, N.Y. 10010 aNSDcNSDdDLCdAIPdNLMdNST0 aAC455.2.P73bP890 aPsychother. priv. pract. 0aPsychotherapy in private practice00a1990 to 2010 a[New York, N.Y.] :bHaworth Press,c[c1983- a17 v. :bill. ;c23 cm. aQuarterly0 aVol. 1, no. 1 (spring 1983)-1 aCeased with: Vol. 17, no. 4 (1998). aTitle from cover. 2aPsychotherapyvperiodicals. 2aPrivate Practicevperiodicals. 0aPsychotherapyxPracticevPeriodicals.00tJournal of psychotherapy in independent practicex1522-9580w(DLC)sn 98005091w(OCoLC)40524650 aSummer 198401361cas a22003611a 45 00100370000000500170003700700160005400800410007001000170011102200140012803000110014203200170015303500160017003700580018604000630024409000170030721000240032422200400034824500160038826000320040430000170043631000140045336200310046750000220049865000640052065000340058450600660061853000420068471000260072685600960075285601170084893600340096569640328-788e-43fc-9c3c-af39e243f3b720091117105437.0cr an ---uuuuu840713c19859999nyuqr1p 0 a0eng d asn 84006638 0 a0748-4518 aJQCRE6 a755730bUSPS aocm10945712 bPlenum Pub. Corp., 233 Spring St., New York, NY 10013 aNSDcNSDdFUGdNSDdNSTdIULdNSTdHULdNSTdEYMdNSTdHUL aAV6001b.J721 aJ. quant. criminol. 0aJournal of quantitative criminology00aABA Journal aNew York :bPlenum,cc1985- av. ;c23 cm. aQuarterly0 aVol. 1, no. 1 (Mar. 1985)- aTitle from cover. 0aCriminal justice, Administration ofxResearchvPeriodicals. 0aCrimexResearchvPeriodicals. aElectronic access restricted to Villanova University patrons. aAlso available on the World Wide Web.2 aLINK (Online service)41zOnline version [v. 15 (1999)-present]uhttp://springerlink.metapress.com/link.asp?id=10258641zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0748-4518&genre=journal aVol. 1, no. 4 (Dec. 1985) LIC01720cas a22004091a 45 00100370000000500170003700700150005400700140006900800410008301000220012402200140014603000110016003200170017103500160018804000790020405000170028321000200030022200300032024500230035026000430037330000250041636200240044150600660046553000420053155000870057355500420066050001030070265000430080570000360084870000350088470000460091971000490096571000340101485601210104885601170116993600240128625a443d1-541f-4784-b11f-905e43e6e41520091117092424.0cr mn ---|||||hd bda b|c|750901c19379999ncuqr1p 0 a0eng d a 40012649 //r86 a0022-3387 aJPRPAU a281800bUSPS aocm01588544 aMULcMULdNSDdDLCdNSDdDLCdm.c.dRCSdAIPdNSDdAIPdOCLdDLCdm/cdNST00aAF1001b.J661 aJ. parapsychol. 0aJournal of parapsychology04aaccidental atheist aDurham, N.C. :bDuke University Press. av. :bill. ;c23 cm.0 aVol. 1 (Mar. 1937)- aElectronic access restricted to Villanova University patrons. aAlso available on the World Wide Web. aIssued in cooperation with the Duke University Parapsychology Laboratory, 194 -50. aVols. 1-16, 1937-52, in v. 17, no. 3. aEditors: Mar. 1937-<Dec. 1938> J. B. Rhine, C. E. Stuart (with W. McDougall, Mar. 1937-Sept. 1938) 0aParapsychologyxResearchvPeriodicals.1 aMcDougall, William,d1871-1938.1 aStuart, Charles Edward,d1907-1 aRhine, J. B.q(Joseph Banks),d1895-1980.2 aDuke University.bParapsychology Laboratory.2 aProQuest Psychology Journals.41zOnline version [v. 59 (1995)-present]uhttp://proquest.umi.com/pqdweb?pmid=000029287&clientId=3260&RQT=318&VName=PQD41zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-3387&genre=journal aMar. 1937-Dec. 195801536cas a22003371a 45 001003700000005001700037008004100054010003700095022001400132035001600146040003800162041002300200050001500223222004100238245002100279260010900300300002000409310002500429321002200454362005700476500002200533546004100555550017300596580007100769650003800840650003000878710004700908710007500955780008101030785008701111f3cf2855-5dfd-486a-bdf3-465e6a29859920091117105533.0751101d19571970inumr1p o0 a0eng aa 53008626 //r853zsc 80001624 a0095-9057 aocm01772601 aDLCcMULdNSDdDLCdm/cdHULdNST0 aengafreageraita0 aAA1b.J975 0aJournal of mathematics and mechanics00aAge of TV heroes aBloomington, Ind. :bGraduate Institute for Mathematics and Mechanics, Indiana University,cc1957-c1970. a14 v. ;c26 cm. aMonthly,b-June 1970 aBimonthly,b1957-0 aVol. 6, no. 1 (Jan. 1957)-v. 19, no. 12 (June 1970). aTitle from cover. aEnglish, French, German and Italian. aVols. for 1957- published by the Graduate Institute for Mathematics and Mechanics, Indiana University; -June 1970 by the Dept. of Mathematics, Indiana University. aContinued in July 1970 by: Indiana University mathematics journal. 0aMechanics, AnalyticvPeriodicals. 0aMathematicsvPeriodicals.2 aIndiana University.bDept. of Mathematics.2 aIndiana University.bGraduate Institute for Mathematics and Mechanics.00tJournal of rational mechanics and analysisw(DLC)sf 85001244w(OCoLC)226307110tIndiana University mathematics journalx0022-2518w(DLC) 75640369w(OCoLC)175301901452cas a22003851a 45 0010037000000050017000370070015000540070014000690080041000830100017001240220014001410300011001550320017001660350016001830370060001990400059002590500014003182100016003322220030003482450032003782600042004103000025004523100022004773210024004993620022005235060066005455000047006115300042006586500029007007000038007297100034007678560122008018560117009239360026010402fb80735-18df-49a9-b4e9-2579c40fd8a720091117102216.0cr mn ---|||||hd bfa b|c|741112c19369999maubr1p 0 a0eng a 38003075 a0022-3980 aJOPSAM a282400bUSPS aocm01782317 bJournal Press, 2 Commercial St., Provincetown, MA 02657 aDLCcDLCdOCLdDLCdNSDdm.c.dOCLdRCSdAIPdOCLdNSD00aAF1b.J671 aJ. psychol. 4aThe Journal of psychology04aAl Gorehby Rebecca Stefoff aProvincetown, Mass. :bJournal Press. av. :bill. ;c25 cm. aBimonthly,b1965- aQuarterly,b1935-640 aVol. 1 (1935/36)- aElectronic access restricted to Villanova University patrons. aEditor: 1935/36-<Oct. 1937,> C. Murchison. aAlso available on the World Wide Web. 0aPsychologyvPeriodicals.1 aMurchison, Carl Allanmore,d1887-2 aProQuest Psychology Journals.41zOnline version [v. 128 (1994)-present]uhttp://proquest.umi.com/pqdweb?pmid=000014346&clientId=3260&RQT=318&VName=PQD41zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-3980&genre=journal aJan. 1975 (surrogate)01267cas a22003251a 45 001003700000005001700037007001500054008004100069010002200110022001400132030001100146035001600157040007300173050001400246210002300260222003800283245003000321260004200351300002500393362002400418506006600442500003000508530004200538650004100580650005200621700001800673710003600691856009700727856011700824d7bcca8e-ec42-464f-8cae-9777a16fb42c20091117102549.0cr an 741112c19569999enkbrzp 0 a0eng a 57003141 //r830 a0022-3999 aJPCRAT aocm01782774 aDLCcDLCdNSDdOCLdNSDdDLCdSERdOCLdRCSdDLCdOCLdAIPdOCLdNSD00aAC52b.J61 aJ. psychosom. res. 0aJournal of psychosomatic research00aAl Gorehby Dale Anderson aLondon ;aNew York :bPergamon Press. av. :bill. ;c26 cm.0 aVol. 1 (Feb. 1956)- aElectronic access restricted to Villanova University patrons. aEditor: 1956- D. Leigh. aAlso available on the World Wide Web. 2aPsychosomatic Medicinevperiodicals. 0aMedicine, PsychosomaticxResearchvPeriodicals.1 aLeigh, Denis.2 aScienceDirect (Online service).41zOnline version [v. 44 (1998)-present]uhttp://www.sciencedirect.com/science/journal/0022399941zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-3999&genre=journal02174cas a2200469 a 45000010037000000050017000370060019000540070015000730080041000880100017001290350023001460400050001690220028002190370088002470500013003350490009003481300037003572100026003942220042004202450032004622600070004943100028005643210036005923620031006285000052006595000033007115000095007445060066008395380036009055500120009416500037010616500022010986500029011207100051011497100057012007100025012577760096012827800075013788560122014538560117015759940012016926d6cbb34-edb4-4577-b148-9a33d8d4130420091117160642.0m d cr mnu||||||||741112c19659999pauqx pss 0 a0eng d a 2006212058 a(OCoLC)ocm39109327 aFGAcFGAdOCLdOCLCQdFQMdNSDdEYMdHULdPVU0 a1559-8519y0022-449921 bSociety for the Scientific Study of Sexuality, PO Box 416, Allentown, PA 18105-041614aAQ5b.J6 aPVUM0 aJournal of sex research (Online)1 aJ. sex res.b(Online) 4aThe journal of sex researchb(Online)14aAl Gorehby Tracey Baptiste aNew York, N.Y. :bSociety for the Scientific Study of Sex,c1965- a4 issues yearly,b1967- aThree issues yearly,b1965-19660 aVol. 1, no. 1 (Mar. 1965)- aTitle from cover (JSTOR, viewed Apr. 18, 2007). aPlace of publication varies. aLatest issue consulted: Vol. 43, issue 4 (Nov. 2006) (SSSS website, viewed Apr. 18, 2007). aElectronic access restricted to Villanova University patrons. aMode of access: World Wide Web. aIssued by: Society for the Scientific Study of Sex, 1965-1994; Society for the Scientific Study of Sexuality, 1995- 0aSexologyxResearchvPeriodicals.12aSexvPeriodicals.22aPsychiatryvPeriodicals.2 aSociety for the Scientific Study of Sex (U.S.)2 aSociety for the Scientific Study of Sexuality (U.S.)2 aJSTOR (Organization)08iAlso issued in print:tJournal of sex researchx0022-4499w(DLC) 68130414w(OCoLC)178336500tAdvances in sex research (Online)w(DLC) 2007234124w(OCoLC)12320877540zOnline version [JSTOR: v. 1 (1965)-present ; latest 5 years unavailable]uhttp://www.jstor.org/journals/00224499.html40zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-4499&genre=journal aC0bPVU01206cas a22003251a 45 00100370000000500170003700700160005400800410007001000170011102200140012803000110014203500160015303700100016904000530017905000180023209000170025021000280026722200430029524500310033826000510036930000170042036200240043750000350046150600660049653000420056265000460060471000200065085600930067085601170076355b3095a-a55b-4411-b532-eb84c47e2c3020091117093002.0cr bn ---|||||751002c19709999njufr1p 0 a0eng a 75643076 0 a0047-2662 aJPHPAE aocm02240975 b07716 aDLCcDLCdNSDdDLCdOCLdRCSdAIPdNSDdOCLdNST0 aAF204.5b.J68 aAF204.5b.J61 aJ. phenomenol. psychol. 0aJournal of phenomenological psychology00aAl Gore and global warming a[Atlantic Highlands, N.J. :bHumanities Press] av. ;c23 cm.0 aVol. 1 (fall 1970)- aAt head of title <1974->: JPP. aElectronic access restricted to Villanova University patrons. aAlso available on the World Wide Web. 0aPhenomenological psychologyvPeriodicals.2 aIngenta (Firm).41zOnline version [v. 1 (1970/71)-present]uhttp://www.ingentaconnect.com/content/brill/jpp41zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0047-2662&genre=journal \ No newline at end of file -- GitLab