Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.72% covered (warning)
78.72%
307 / 390
58.72% covered (warning)
58.72%
64 / 109
CRAP
0.00% covered (danger)
0.00%
0 / 1
DefaultRecord
78.72% covered (warning)
78.72%
307 / 390
58.72% covered (warning)
58.72%
64 / 109
635.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAccessRestrictions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllSubjectHeadings
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getAllRecordLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAuthorDataFields
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 getAwards
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBibliographyNotes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBreadcrumb
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCallNumber
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCallNumbers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCleanDOI
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getCleanISBN
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
5.15
 getCleanISBNs
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
10
 getCleanISSN
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getCleanOCLCNum
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getCleanUPC
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getCleanNBN
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCleanISMN
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCorporateAuthors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCorporateAuthorsRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDateSpan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeduplicatedAuthors
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
 getEdition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFindingAids
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFormats
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGeneralNotes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRawAuthorHighlights
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryAuthorsWithHighlighting
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
4.12
 getLastIndexed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSnippetCaption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHighlightedSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHighlightedTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInstitutions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBuildings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getISBNs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getISSNs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRawLCCN
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLCCN
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
4.12
 getNewerTitles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOCLC
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOpenUrlFormat
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
9.80
 getCoinsID
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
9.66
 getDefaultOpenUrlParams
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getBookOpenUrlParams
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
5.00
 getArticleOpenUrlParams
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 getUnknownFormatOpenUrlParams
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getJournalOpenUrlParams
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 getOpenUrl
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
6.02
 getCoinsOpenUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPhysicalDescriptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPlacesOfPublication
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPlayingTimes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPreviousTitles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryAuthor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryAuthors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryAuthorsRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProductionCredits
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPublicationDates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHumanReadablePublicationDates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPublicationDetails
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 getPublicationFrequency
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPublishers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRealTimeHistory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRealTimeHoldings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRelationshipNotes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSecondaryAuthors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSecondaryAuthorsRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSeries
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getShortTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSource
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSubtitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSystemDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSummary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTargetAudienceNotes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbnail
73.33% covered (warning)
73.33%
22 / 30
0.00% covered (danger)
0.00%
0 / 1
16.20
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitleSection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleStatement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTOC
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHierarchicalPlaceNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUPC
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUuids
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCleanUuid
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getURLs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getHierarchyTopID
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHierarchyTopTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContainingCollections
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCollection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHierarchyTrees
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHierarchyType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUniqueID
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getXML
90.62% covered (success)
90.62%
29 / 32
0.00% covered (danger)
0.00%
0 / 1
10.08
 getSupportedCitationFormats
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainerTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainerVolume
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainerIssue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainerStartPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContainerEndPage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContainerReference
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSortTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSchemaOrgFormatsArray
28.57% covered (danger)
28.57%
6 / 21
0.00% covered (danger)
0.00%
0 / 1
46.44
 getSchemaOrgFormats
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDedupData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getChildRecordCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContainerRecordID
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGeoLocation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDisplayCoordinates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCoordinateLabels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Default model for records
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  RecordDrivers
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
28 */
29
30namespace VuFind\RecordDriver;
31
32use VuFind\View\Helper\Root\RecordLinker;
33use VuFindCode\ISBN;
34
35use function count;
36use function in_array;
37use function is_array;
38use function strlen;
39
40/**
41 * Default model for records
42 *
43 * @category VuFind
44 * @package  RecordDrivers
45 * @author   Demian Katz <demian.katz@villanova.edu>
46 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
47 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
48 *
49 * @SuppressWarnings(PHPMD.ExcessivePublicCount)
50 */
51class DefaultRecord extends AbstractBase
52{
53    /**
54     * Should we highlight fields in search results?
55     *
56     * @var bool
57     */
58    protected $highlight = false;
59
60    /**
61     * Constructor
62     *
63     * @param \Laminas\Config\Config $mainConfig     VuFind main configuration (omit
64     * for built-in defaults)
65     * @param \Laminas\Config\Config $recordConfig   Record-specific configuration
66     * file (omit to use $mainConfig as $recordConfig)
67     * @param \Laminas\Config\Config $searchSettings Search-specific configuration
68     * file
69     */
70    public function __construct(
71        $mainConfig = null,
72        $recordConfig = null,
73        $searchSettings = null
74    ) {
75        // Turn on highlighting as needed:
76        $this->highlight = $searchSettings->General->highlighting ?? false;
77
78        parent::__construct($mainConfig, $recordConfig);
79    }
80
81    /**
82     * Get access restriction notes for the record.
83     *
84     * @return array
85     */
86    public function getAccessRestrictions()
87    {
88        // Not currently stored in the default index schema
89        return [];
90    }
91
92    /**
93     * Get all subject headings associated with this record. Each heading is
94     * returned as an array of chunks, increasing from least specific to most
95     * specific.
96     *
97     * @param bool $extended Whether to return a keyed array with the following
98     * keys:
99     * - heading: the actual subject heading chunks
100     * - type: heading type
101     * - source: source vocabulary
102     *
103     * @return array
104     */
105    public function getAllSubjectHeadings($extended = false)
106    {
107        $headings = [];
108        foreach (['topic', 'geographic', 'genre', 'era'] as $field) {
109            if (isset($this->fields[$field])) {
110                $headings = array_merge($headings, $this->fields[$field]);
111            }
112        }
113
114        // The default index schema doesn't currently store subject headings in a
115        // broken-down format, so we'll just send each value as a single chunk.
116        // Other record drivers (i.e. SolrMarc) can offer this data in a more
117        // granular format.
118        $callback = function ($i) use ($extended) {
119            return $extended
120                ? ['heading' => [$i], 'type' => '', 'source' => '']
121                : [$i];
122        };
123        return array_map($callback, array_unique($headings));
124    }
125
126    /**
127     * Get all record links related to the current record. Each link is returned as
128     * array.
129     * NB: to use this method you must override it.
130     * Format:
131     * <code>
132     * array(
133     *        array(
134     *               'title' => label_for_title
135     *               'value' => link_name
136     *               'link'  => link_URI
137     *        ),
138     *        ...
139     * )
140     * </code>
141     *
142     * @return null|array
143     */
144    public function getAllRecordLinks()
145    {
146        return null;
147    }
148
149    /**
150     * Get Author Information with Associated Data Fields
151     *
152     * @param string $index      The author index [primary, corporate, or secondary]
153     * used to construct a method name for retrieving author data (e.g.
154     * getPrimaryAuthors).
155     * @param array  $dataFields An array of fields to used to construct method
156     * names for retrieving author-related data (e.g., if you pass 'role' the
157     * data method will be similar to getPrimaryAuthorsRoles). This value will also
158     * be used as a key associated with each author in the resulting data array.
159     *
160     * @return array
161     */
162    public function getAuthorDataFields($index, $dataFields = [])
163    {
164        $data = $dataFieldValues = [];
165
166        // Collect author data
167        $authorMethod = sprintf('get%sAuthors', ucfirst($index));
168        $authors = $this->tryMethod($authorMethod, [], []);
169
170        // Collect attribute data
171        foreach ($dataFields as $field) {
172            $fieldMethod = $authorMethod . ucfirst($field) . 's';
173            $dataFieldValues[$field] = $this->tryMethod($fieldMethod, [], []);
174        }
175
176        // Match up author and attribute data (this assumes that the attribute
177        // arrays have the same indices as the author array; i.e. $author[$i]
178        // has $dataFieldValues[$attribute][$i].
179        foreach ($authors as $i => $author) {
180            if (!isset($data[$author])) {
181                $data[$author] = [];
182            }
183
184            foreach ($dataFieldValues as $field => $dataFieldValue) {
185                if (!empty($dataFieldValue[$i])) {
186                    $data[$author][$field][] = $dataFieldValue[$i];
187                }
188            }
189        }
190
191        return $data;
192    }
193
194    /**
195     * Get award notes for the record.
196     *
197     * @return array
198     */
199    public function getAwards()
200    {
201        // Not currently stored in the default index schema
202        return [];
203    }
204
205    /**
206     * Get notes on bibliography content.
207     *
208     * @return array
209     */
210    public function getBibliographyNotes()
211    {
212        // Not currently stored in the default index schema
213        return [];
214    }
215
216    /**
217     * Get text that can be displayed to represent this record in
218     * breadcrumbs.
219     *
220     * @return string Breadcrumb text to represent this record.
221     */
222    public function getBreadcrumb()
223    {
224        return $this->getShortTitle();
225    }
226
227    /**
228     * Get the first call number associated with the record (empty string if none).
229     *
230     * @return string
231     */
232    public function getCallNumber()
233    {
234        $all = $this->getCallNumbers();
235        return $all[0] ?? '';
236    }
237
238    /**
239     * Get all call numbers associated with the record.
240     *
241     * @return array
242     */
243    public function getCallNumbers()
244    {
245        return (array)($this->fields['callnumber-raw'] ?? []);
246    }
247
248    /**
249     * Return the first valid DOI found in the record (false if none).
250     *
251     * @return mixed
252     */
253    public function getCleanDOI()
254    {
255        $field = 'doi_str_mv';
256        return !empty($this->fields[$field][0]) ? $this->fields[$field][0] : false;
257    }
258
259    /**
260     * Return the first valid ISBN found in the record (favoring ISBN-10 over
261     * ISBN-13 when possible).
262     *
263     * @return mixed
264     */
265    public function getCleanISBN()
266    {
267        // Get all the ISBNs and initialize the return value:
268        $isbns = $this->getISBNs();
269        $isbn13 = false;
270
271        // Loop through the ISBNs:
272        foreach ($isbns as $isbn) {
273            // Strip off any unwanted notes:
274            if ($pos = strpos($isbn, ' ')) {
275                $isbn = substr($isbn, 0, $pos);
276            }
277
278            // If we find an ISBN-10, return it immediately; otherwise, if we find
279            // an ISBN-13, save it if it is the first one encountered.
280            $isbnObj = new ISBN($isbn);
281            if ($isbn10 = $isbnObj->get10()) {
282                return $isbn10;
283            }
284            if (!$isbn13) {
285                $isbn13 = $isbnObj->get13();
286            }
287        }
288        return $isbn13;
289    }
290
291    /**
292     * Return all ISBNs found in the record.
293     *
294     * @param string $mode          Mode for returning ISBNs:
295     *  - 'only10' returns only ISBN-10s
296     *  - 'prefer10' returns ISBN-10s if available, otherwise ISBN-13s (default)
297     *  - 'normalize13' returns ISBN-13s, normalizing ISBN-10s to ISBN-13s
298     * @param bool   $filterInvalid Whether to filter out invalid ISBNs
299     *
300     * @return array
301     */
302    public function getCleanISBNs(
303        string $mode = 'prefer10',
304        bool $filterInvalid = true
305    ): array {
306        $isbns = $this->getISBNs();
307        $all = $tens = $thirteens = $invalid = [];
308        foreach ($isbns as $isbn) {
309            // Strip off any unwanted notes:
310            if ($pos = strpos($isbn, ' ')) {
311                $isbn = substr($isbn, 0, $pos);
312            }
313            $isbnObj = new ISBN($isbn);
314            if ($isbnObj->isValid()) {
315                if ($isbn10 = $isbnObj->get10()) {
316                    $normalized
317                        = $mode === 'normalize13' ? $isbnObj->get13() : $isbn10;
318                    $tens[] = $normalized;
319                    $all[] = $normalized;
320                } elseif ($isbn13 = $isbnObj->get13()) {
321                    $thirteens[] = $isbn13;
322                    $all[] = $isbn13;
323                }
324            } elseif (!$filterInvalid) {
325                $invalid[] = $isbn;
326                $all[] = $isbn;
327            }
328        }
329        if ($mode === 'only10') {
330            return array_merge($tens, $invalid);
331        }
332        return $mode === 'prefer10'
333            ? array_merge($tens, $thirteens, $invalid) : $all;
334    }
335
336    /**
337     * Get just the base portion of the first listed ISSN (or false if no ISSNs).
338     *
339     * @return mixed
340     */
341    public function getCleanISSN()
342    {
343        $issns = $this->getISSNs();
344        if (empty($issns)) {
345            return false;
346        }
347        $issn = $issns[0];
348        if ($pos = strpos($issn, ' ')) {
349            $issn = substr($issn, 0, $pos);
350        }
351        return $issn;
352    }
353
354    /**
355     * Get just the first listed OCLC Number (or false if none available).
356     *
357     * @return mixed
358     */
359    public function getCleanOCLCNum()
360    {
361        $nums = $this->getOCLC();
362        return empty($nums) ? false : $nums[0];
363    }
364
365    /**
366     * Get just the first listed UPC Number (or false if none available).
367     *
368     * @return mixed
369     */
370    public function getCleanUPC()
371    {
372        $nums = $this->getUPC();
373        return empty($nums) ? false : $nums[0];
374    }
375
376    /**
377     * Get just the first listed national bibliography number (or false if none
378     * available).
379     *
380     * @return mixed
381     */
382    public function getCleanNBN()
383    {
384        return false;
385    }
386
387    /**
388     * Get just the base portion of the first listed ISMN (or false if no ISSMs).
389     *
390     * @return mixed
391     */
392    public function getCleanISMN()
393    {
394        return false;
395    }
396
397    /**
398     * Get the main corporate authors (if any) for the record.
399     *
400     * @return array
401     */
402    public function getCorporateAuthors()
403    {
404        return (array)($this->fields['author_corporate'] ?? []);
405    }
406
407    /**
408     * Get an array of all main corporate authors roles.
409     *
410     * @return array
411     */
412    public function getCorporateAuthorsRoles()
413    {
414        return (array)($this->fields['author_corporate_role'] ?? []);
415    }
416
417    /**
418     * Get the date coverage for a record which spans a period of time (i.e. a
419     * journal). Use getPublicationDates for publication dates of particular
420     * monographic items.
421     *
422     * @return array
423     */
424    public function getDateSpan()
425    {
426        return (array)($this->fields['dateSpan'] ?? []);
427    }
428
429    /**
430     * Deduplicate author information into associative array with main/corporate/
431     * secondary keys.
432     *
433     * @param array $dataFields An array of extra data fields to retrieve (see
434     * getAuthorDataFields)
435     *
436     * @return array
437     */
438    public function getDeduplicatedAuthors($dataFields = ['role'])
439    {
440        $authors = [];
441        foreach (['primary', 'secondary', 'corporate'] as $type) {
442            $authors[$type] = $this->getAuthorDataFields($type, $dataFields);
443        }
444
445        // deduplicate
446        $dedup = function (&$array1, &$array2) {
447            if (!empty($array1) && !empty($array2)) {
448                $keys = array_keys($array1);
449                foreach ($keys as $author) {
450                    if (isset($array2[$author])) {
451                        $array1[$author] = array_merge_recursive(
452                            $array1[$author],
453                            $array2[$author]
454                        );
455                        unset($array2[$author]);
456                    }
457                }
458            }
459        };
460
461        $dedup($authors['primary'], $authors['corporate']);
462        $dedup($authors['secondary'], $authors['corporate']);
463        $dedup($authors['primary'], $authors['secondary']);
464
465        $dedup_data = function (&$array) {
466            foreach ($array as $author => $data) {
467                foreach ($data as $field => $values) {
468                    if (is_array($values)) {
469                        $array[$author][$field] = array_unique($values);
470                    }
471                }
472            }
473        };
474
475        $dedup_data($authors['primary']);
476        $dedup_data($authors['secondary']);
477        $dedup_data($authors['corporate']);
478
479        return $authors;
480    }
481
482    /**
483     * Get the edition of the current record.
484     *
485     * @return string
486     */
487    public function getEdition()
488    {
489        return $this->fields['edition'] ?? '';
490    }
491
492    /**
493     * Get notes on finding aids related to the record.
494     *
495     * @return array
496     */
497    public function getFindingAids()
498    {
499        // Not currently stored in the default index schema
500        return [];
501    }
502
503    /**
504     * Get an array of all the formats associated with the record.
505     *
506     * @return array
507     */
508    public function getFormats()
509    {
510        return (array)($this->fields['format'] ?? []);
511    }
512
513    /**
514     * Get general notes on the record.
515     *
516     * @return array
517     */
518    public function getGeneralNotes()
519    {
520        // Not currently stored in the default index schema
521        return [];
522    }
523
524    /**
525     * Get highlighted author data, if available.
526     *
527     * @return array
528     */
529    public function getRawAuthorHighlights()
530    {
531        // Not supported by default.
532        return [];
533    }
534
535    /**
536     * Get primary author information with highlights applied (if applicable)
537     *
538     * @return array
539     */
540    public function getPrimaryAuthorsWithHighlighting()
541    {
542        $highlights = [];
543        // Create a map of de-highlighted values => highlighted values.
544        foreach ($this->getRawAuthorHighlights() as $current) {
545            $dehighlighted = str_replace(
546                ['{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'],
547                '',
548                $current
549            );
550            $highlights[$dehighlighted] = $current;
551        }
552
553        // replace unhighlighted authors with highlighted versions where
554        // applicable:
555        $authors = [];
556        foreach ($this->getPrimaryAuthors() as $author) {
557            $authors[] = $highlights[$author] ?? $author;
558        }
559        return $authors;
560    }
561
562    /**
563     * Get a string representing the last date that the record was indexed.
564     *
565     * @return string
566     */
567    public function getLastIndexed()
568    {
569        return $this->fields['last_indexed'] ?? '';
570    }
571
572    /**
573     * Given a field name, return an appropriate caption.
574     *
575     * @param string $field Field name
576     *
577     * @return mixed        Caption if found, false if none available.
578     *
579     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
580     */
581    public function getSnippetCaption($field)
582    {
583        // Not supported by default.
584        return false;
585    }
586
587    /**
588     * Pick one line from the highlighted text (if any) to use as a snippet.
589     *
590     * @return mixed False if no snippet found, otherwise associative array
591     * with 'snippet' and 'caption' keys.
592     */
593    public function getHighlightedSnippet()
594    {
595        // Not supported by default.
596        return false;
597    }
598
599    /**
600     * Get a highlighted title string, if available.
601     *
602     * @return string
603     */
604    public function getHighlightedTitle()
605    {
606        // Not supported by default.
607        return '';
608    }
609
610    /**
611     * Get the institutions holding the record.
612     *
613     * @return array
614     */
615    public function getInstitutions()
616    {
617        return (array)($this->fields['institution'] ?? []);
618    }
619
620    /**
621     * Get the buildings containing the record.
622     *
623     * @return array
624     */
625    public function getBuildings()
626    {
627        return (array)($this->fields['building'] ?? []);
628    }
629
630    /**
631     * Get an array of all ISBNs associated with the record (may be empty).
632     *
633     * @return array
634     */
635    public function getISBNs()
636    {
637        return (array)($this->fields['isbn'] ?? []);
638    }
639
640    /**
641     * Get an array of all ISSNs associated with the record (may be empty).
642     *
643     * @return array
644     */
645    public function getISSNs()
646    {
647        return (array)($this->fields['issn'] ?? []);
648    }
649
650    /**
651     * Get an array of all the languages associated with the record.
652     *
653     * @return array
654     */
655    public function getLanguages()
656    {
657        return (array)($this->fields['language'] ?? []);
658    }
659
660    /**
661     * Get a raw, unnormalized LCCN. (See getLCCN for normalization).
662     *
663     * @return string
664     */
665    protected function getRawLCCN()
666    {
667        // Get LCCN from Index
668        return $this->fields['lccn'] ?? '';
669    }
670
671    /**
672     * Get a LCCN, normalised according to info:lccn
673     *
674     * @return string
675     */
676    public function getLCCN()
677    {
678        // Remove all blanks.
679        $raw = preg_replace('{[ \t]+}', '', $this->getRawLCCN());
680
681        // If there is a forward slash (/) in the string, remove it, and remove all
682        // characters to the right of the forward slash.
683        if (strpos($raw, '/') > 0) {
684            $tmpArray = explode('/', $raw);
685            $raw = $tmpArray[0];
686        }
687        /* If there is a hyphen in the string:
688            a. Remove it.
689            b. Inspect the substring following (to the right of) the (removed)
690               hyphen. Then (and assuming that steps 1 and 2 have been carried out):
691                    i. All these characters should be digits, and there should be
692                    six or less.
693                    ii. If the length of the substring is less than 6, left-fill the
694                    substring with zeros until  the length is six.
695        */
696        if (strpos($raw, '-') > 0) {
697            // haven't checked for i. above. If they aren't all digits, there is
698            // nothing that can be done, so might as well leave it.
699            $tmpArray = explode('-', $raw);
700            $raw = $tmpArray[0] . str_pad($tmpArray[1], 6, '0', STR_PAD_LEFT);
701        }
702        return $raw;
703    }
704
705    /**
706     * Get an array of newer titles for the record.
707     *
708     * @return array
709     */
710    public function getNewerTitles()
711    {
712        return (array)($this->fields['title_new'] ?? []);
713    }
714
715    /**
716     * Get the OCLC number(s) of the record.
717     *
718     * @return array
719     */
720    public function getOCLC()
721    {
722        return (array)($this->fields['oclc_num'] ?? []);
723    }
724
725    /**
726     * Support method for getOpenUrl() -- pick the OpenURL format.
727     *
728     * @return string
729     */
730    protected function getOpenUrlFormat()
731    {
732        // If we have multiple formats, Book, Journal and Article are most
733        // important...
734        $formats = $this->getFormats();
735        if (in_array('Book', $formats) || in_array('eBook', $formats)) {
736            return 'Book';
737        } elseif (in_array('Article', $formats)) {
738            return 'Article';
739        } elseif (in_array('Journal', $formats)) {
740            return 'Journal';
741        } elseif (strlen($this->getCleanISSN()) > 0) {
742            // If the record has an ISSN and we have not already
743            // decided it is an Article, we'll treat it as a Book
744            // if it has an ISBN and is therefore likely part of a
745            // monographic series. Otherwise, we'll treat it as a
746            // Journal.
747            // Anecdotally, some link resolvers do not return correct
748            // results when given both ISBN and ISSN for a member of a
749            // monographic series.
750            return strlen($this->getCleanISBN()) > 0 ? 'Book' : 'Journal';
751        } elseif (isset($formats[0])) {
752            return $formats[0];
753        } elseif (strlen($this->getCleanISBN()) > 0) {
754            // Last ditch. Note that this is last by intention; if the
755            // record has a format set and also has an ISBN, we don't
756            // necessarily want to send the ISBN, as it may be a game
757            // or a DVD that wouldn't typically be found in OpenURL
758            // knowledgebases.
759            return 'Book';
760        }
761        return 'UnknownFormat';
762    }
763
764    /**
765     * Get the COinS identifier.
766     *
767     * @return string
768     */
769    protected function getCoinsID()
770    {
771        // Get the COinS ID -- it should be in the OpenURL section of config.ini,
772        // but we'll also check the COinS section for compatibility with legacy
773        // configurations (this moved between the RC2 and 1.0 releases).
774        if (
775            isset($this->mainConfig->OpenURL->rfr_id)
776            && !empty($this->mainConfig->OpenURL->rfr_id)
777        ) {
778            return $this->mainConfig->OpenURL->rfr_id;
779        }
780        if (
781            isset($this->mainConfig->COinS->identifier)
782            && !empty($this->mainConfig->COinS->identifier)
783        ) {
784            return $this->mainConfig->COinS->identifier;
785        }
786        return 'vufind.svn.sourceforge.net';
787    }
788
789    /**
790     * Get default OpenURL parameters.
791     *
792     * @return array
793     */
794    protected function getDefaultOpenUrlParams()
795    {
796        // Get a representative publication date:
797        $pubDate = $this->getPublicationDates();
798        $pubDate = empty($pubDate) ? '' : $pubDate[0];
799
800        // Start an array of OpenURL parameters:
801        return [
802            'url_ver' => 'Z39.88-2004',
803            'ctx_ver' => 'Z39.88-2004',
804            'ctx_enc' => 'info:ofi/enc:UTF-8',
805            'rfr_id' => 'info:sid/' . $this->getCoinsID() . ':generator',
806            'rft.title' => $this->getTitle(),
807            'rft.date' => $pubDate,
808        ];
809    }
810
811    /**
812     * Get OpenURL parameters for a book.
813     *
814     * @return array
815     */
816    protected function getBookOpenUrlParams()
817    {
818        $params = $this->getDefaultOpenUrlParams();
819        $params['rft_val_fmt'] = 'info:ofi/fmt:kev:mtx:book';
820        $params['rft.genre'] = 'book';
821        $params['rft.btitle'] = $params['rft.title'];
822        $series = $this->getSeries();
823        if (count($series) > 0) {
824            // Handle both possible return formats of getSeries:
825            $params['rft.series'] = is_array($series[0]) ?
826                $series[0]['name'] : $series[0];
827        }
828        $params['rft.au'] = $this->getPrimaryAuthor();
829        $publishers = $this->getPublishers();
830        if (count($publishers) > 0) {
831            $params['rft.pub'] = $publishers[0];
832        }
833        $placesOfPublication = $this->getPlacesOfPublication();
834        if (count($placesOfPublication) > 0) {
835            $params['rft.place'] = $placesOfPublication[0];
836        }
837        $params['rft.edition'] = $this->getEdition();
838        $params['rft.isbn'] = (string)$this->getCleanISBN();
839        return $params;
840    }
841
842    /**
843     * Get OpenURL parameters for an article.
844     *
845     * @return array
846     */
847    protected function getArticleOpenUrlParams()
848    {
849        $params = $this->getDefaultOpenUrlParams();
850        $params['rft_val_fmt'] = 'info:ofi/fmt:kev:mtx:journal';
851        $params['rft.genre'] = 'article';
852        $params['rft.issn'] = (string)$this->getCleanISSN();
853        // an article may have also an ISBN:
854        $params['rft.isbn'] = (string)$this->getCleanISBN();
855        $params['rft.volume'] = $this->getContainerVolume();
856        $params['rft.issue'] = $this->getContainerIssue();
857        $params['rft.spage'] = $this->getContainerStartPage();
858        // unset default title -- we only want jtitle/atitle here:
859        unset($params['rft.title']);
860        $params['rft.jtitle'] = $this->getContainerTitle();
861        $params['rft.atitle'] = $this->getTitle();
862        $params['rft.au'] = $this->getPrimaryAuthor();
863
864        $params['rft.format'] = 'Article';
865        $langs = $this->getLanguages();
866        if (count($langs) > 0) {
867            $params['rft.language'] = $langs[0];
868        }
869        return $params;
870    }
871
872    /**
873     * Get OpenURL parameters for an unknown format.
874     *
875     * @param string $format Name of format
876     *
877     * @return array
878     */
879    protected function getUnknownFormatOpenUrlParams($format = 'UnknownFormat')
880    {
881        $params = $this->getDefaultOpenUrlParams();
882        $params['rft_val_fmt'] = 'info:ofi/fmt:kev:mtx:dc';
883        $params['rft.creator'] = $this->getPrimaryAuthor();
884        $publishers = $this->getPublishers();
885        if (count($publishers) > 0) {
886            $params['rft.pub'] = $publishers[0];
887        }
888        $params['rft.format'] = $format;
889        $langs = $this->getLanguages();
890        if (count($langs) > 0) {
891            $params['rft.language'] = $langs[0];
892        }
893        return $params;
894    }
895
896    /**
897     * Get OpenURL parameters for a journal.
898     *
899     * @return array
900     */
901    protected function getJournalOpenUrlParams()
902    {
903        $params = $this->getUnknownFormatOpenUrlParams('Journal');
904        /* This is probably the most technically correct way to represent
905         * a journal run as an OpenURL; however, it doesn't work well with
906         * Zotero, so it is currently commented out -- instead, we just add
907         * some extra fields and to the "unknown format" case.
908        $params['rft_val_fmt'] = 'info:ofi/fmt:kev:mtx:journal';
909        $params['rft.genre'] = 'journal';
910        $params['rft.jtitle'] = $params['rft.title'];
911        $params['rft.issn'] = $this->getCleanISSN();
912        $params['rft.au'] = $this->getPrimaryAuthor();
913         */
914        $params['rft.issn'] = (string)$this->getCleanISSN();
915
916        // Including a date in a title-level Journal OpenURL may be too
917        // limiting -- in some link resolvers, it may cause the exclusion
918        // of databases if they do not cover the exact date provided!
919        unset($params['rft.date']);
920
921        // If we're working with the SFX or Alma resolver, we should add a
922        // special parameter to ensure that electronic holdings links
923        // are shown even though no specific date or issue is specified:
924        $resolver = strtolower($this->mainConfig->OpenURL->resolver ?? '');
925        if ('sfx' === $resolver) {
926            $params['sfx.ignore_date_threshold'] = 1;
927        } elseif ('alma' === $resolver) {
928            $params['u.ignore_date_coverage'] = 'true';
929        }
930        return $params;
931    }
932
933    /**
934     * Get the OpenURL parameters to represent this record (useful for the
935     * title attribute of a COinS span tag).
936     *
937     * @param bool $overrideSupportsOpenUrl Flag to override checking
938     * supportsOpenUrl() (default is false)
939     *
940     * @return string OpenURL parameters.
941     */
942    public function getOpenUrl($overrideSupportsOpenUrl = false)
943    {
944        // stop here if this record does not support OpenURLs
945        if (!$overrideSupportsOpenUrl && !$this->supportsOpenUrl()) {
946            return false;
947        }
948
949        // Set up parameters based on the format of the record:
950        $format = $this->getOpenUrlFormat();
951        $method = "get{$format}OpenUrlParams";
952        if (method_exists($this, $method)) {
953            $params = $this->$method();
954        } else {
955            $params = $this->getUnknownFormatOpenUrlParams($format);
956        }
957
958        // Assemble the URL:
959        $query = [];
960        foreach ($params as $key => $value) {
961            $value = (array)$value;
962            foreach ($value as $sub) {
963                $query[] = urlencode($key) . '=' . urlencode($sub);
964            }
965        }
966        return implode('&', $query);
967    }
968
969    /**
970     * Get the OpenURL parameters to represent this record for COinS even if
971     * supportsOpenUrl() is false for this RecordDriver.
972     *
973     * @return string OpenURL parameters.
974     */
975    public function getCoinsOpenUrl()
976    {
977        return $this->getOpenUrl($this->supportsCoinsOpenUrl());
978    }
979
980    /**
981     * Get an array of physical descriptions of the item.
982     *
983     * @return array
984     */
985    public function getPhysicalDescriptions()
986    {
987        return (array)($this->fields['physical'] ?? []);
988    }
989
990    /**
991     * Get the item's place of publication.
992     *
993     * @return array
994     */
995    public function getPlacesOfPublication()
996    {
997        // Not currently stored in the default index schema
998        return [];
999    }
1000
1001    /**
1002     * Get an array of playing times for the record (if applicable).
1003     *
1004     * @return array
1005     */
1006    public function getPlayingTimes()
1007    {
1008        // Not currently stored in the default index schema
1009        return [];
1010    }
1011
1012    /**
1013     * Get an array of previous titles for the record.
1014     *
1015     * @return array
1016     */
1017    public function getPreviousTitles()
1018    {
1019        return (array)($this->fields['title_old'] ?? []);
1020    }
1021
1022    /**
1023     * Get the main author of the record.
1024     *
1025     * @return string
1026     */
1027    public function getPrimaryAuthor()
1028    {
1029        $authors = $this->getPrimaryAuthors();
1030        return $authors[0] ?? '';
1031    }
1032
1033    /**
1034     * Get the main authors of the record.
1035     *
1036     * @return array
1037     */
1038    public function getPrimaryAuthors()
1039    {
1040        return (array)($this->fields['author'] ?? []);
1041    }
1042
1043    /**
1044     * Get an array of all main authors roles (complementing
1045     * getSecondaryAuthorsRoles()).
1046     *
1047     * @return array
1048     */
1049    public function getPrimaryAuthorsRoles()
1050    {
1051        return (array)($this->fields['author_role'] ?? []);
1052    }
1053
1054    /**
1055     * Get credits of people involved in production of the item.
1056     *
1057     * @return array
1058     */
1059    public function getProductionCredits()
1060    {
1061        // Not currently stored in the default index schema
1062        return [];
1063    }
1064
1065    /**
1066     * Get the publication dates of the record.  See also getDateSpan().
1067     *
1068     * @return array
1069     */
1070    public function getPublicationDates()
1071    {
1072        return (array)($this->fields['publishDate'] ?? []);
1073    }
1074
1075    /**
1076     * Get human readable publication dates for display purposes (may not be suitable
1077     * for computer processing -- use getPublicationDates() for that).
1078     *
1079     * @return array
1080     */
1081    public function getHumanReadablePublicationDates()
1082    {
1083        return $this->getPublicationDates();
1084    }
1085
1086    /**
1087     * Get an array of publication detail lines combining information from
1088     * getPublicationDates(), getPublishers() and getPlacesOfPublication().
1089     *
1090     * @return array
1091     */
1092    public function getPublicationDetails()
1093    {
1094        $places = $this->getPlacesOfPublication();
1095        $names = $this->getPublishers();
1096        $dates = $this->getHumanReadablePublicationDates();
1097
1098        $i = 0;
1099        $retval = [];
1100        while (isset($places[$i]) || isset($names[$i]) || isset($dates[$i])) {
1101            // Build objects to represent each set of data; these will
1102            // transform seamlessly into strings in the view layer.
1103            $retval[] = new Response\PublicationDetails(
1104                $places[$i] ?? '',
1105                $names[$i] ?? '',
1106                $dates[$i] ?? ''
1107            );
1108            $i++;
1109        }
1110
1111        return $retval;
1112    }
1113
1114    /**
1115     * Get an array of publication frequency information.
1116     *
1117     * @return array
1118     */
1119    public function getPublicationFrequency()
1120    {
1121        // Not currently stored in the default index schema
1122        return [];
1123    }
1124
1125    /**
1126     * Get the publishers of the record.
1127     *
1128     * @return array
1129     */
1130    public function getPublishers()
1131    {
1132        return (array)($this->fields['publisher'] ?? []);
1133    }
1134
1135    /**
1136     * Get an array of information about record history, obtained in real-time
1137     * from the ILS.
1138     *
1139     * @return array
1140     */
1141    public function getRealTimeHistory()
1142    {
1143        // Not supported by the default index schema -- implement in child classes.
1144        return [];
1145    }
1146
1147    /**
1148     * Get an array of information about record holdings, obtained in real-time
1149     * from the ILS.
1150     *
1151     * @return array
1152     */
1153    public function getRealTimeHoldings()
1154    {
1155        // Not supported by the default index schema -- implement in child classes.
1156        return ['holdings' => []];
1157    }
1158
1159    /**
1160     * Get an array of strings describing relationships to other items.
1161     *
1162     * @return array
1163     */
1164    public function getRelationshipNotes()
1165    {
1166        // Not currently stored in the default index schema
1167        return [];
1168    }
1169
1170    /**
1171     * Get an array of all secondary authors (complementing getPrimaryAuthors()).
1172     *
1173     * @return array
1174     */
1175    public function getSecondaryAuthors()
1176    {
1177        return (array)($this->fields['author2'] ?? []);
1178    }
1179
1180    /**
1181     * Get an array of all secondary authors roles (complementing
1182     * getPrimaryAuthorsRoles()).
1183     *
1184     * @return array
1185     */
1186    public function getSecondaryAuthorsRoles()
1187    {
1188        return (array)($this->fields['author2_role'] ?? []);
1189    }
1190
1191    /**
1192     * Get an array of all series names containing the record. Array entries may
1193     * be either the name string, or an associative array with 'name' and 'number'
1194     * keys.
1195     *
1196     * @return array
1197     */
1198    public function getSeries()
1199    {
1200        // Only use the contents of the series2 field if the series field is empty
1201        return !empty($this->fields['series'])
1202            ? (array)$this->fields['series']
1203            : (array)($this->fields['series2'] ?? []);
1204    }
1205
1206    /**
1207     * Get the short (pre-subtitle) title of the record.
1208     *
1209     * @return string
1210     */
1211    public function getShortTitle()
1212    {
1213        return $this->fields['title_short'] ?? '';
1214    }
1215
1216    /**
1217     * Get the item's source.
1218     *
1219     * @return string
1220     */
1221    public function getSource()
1222    {
1223        // Not supported in base class:
1224        return '';
1225    }
1226
1227    /**
1228     * Get the subtitle of the record.
1229     *
1230     * @return string
1231     */
1232    public function getSubtitle()
1233    {
1234        return $this->fields['title_sub'] ?? '';
1235    }
1236
1237    /**
1238     * Get an array of technical details on the item represented by the record.
1239     *
1240     * @return array
1241     */
1242    public function getSystemDetails()
1243    {
1244        // Not currently stored in the default index schema
1245        return [];
1246    }
1247
1248    /**
1249     * Get an array of summary strings for the record.
1250     *
1251     * @return array
1252     */
1253    public function getSummary()
1254    {
1255        // We need to return an array, so if we have a description, turn it into an
1256        // array (it should be a flat string according to the default schema, but we
1257        // might as well support the array case just to be on the safe side:
1258        return (array)($this->fields['description'] ?? []);
1259    }
1260
1261    /**
1262     * Get an array of note about the record's target audience.
1263     *
1264     * @return array
1265     */
1266    public function getTargetAudienceNotes()
1267    {
1268        // Not currently stored in the default index schema
1269        return [];
1270    }
1271
1272    /**
1273     * Returns one of three things: a full URL to a thumbnail preview of the record
1274     * if an image is available in an external system; an array of parameters to
1275     * send to VuFind's internal cover generator if no fixed URL exists; or false
1276     * if no thumbnail can be generated.
1277     *
1278     * @param string $size Size of thumbnail (small, medium or large -- small is
1279     * default).
1280     *
1281     * @return string|array|bool
1282     */
1283    public function getThumbnail($size = 'small')
1284    {
1285        if (!empty($this->fields['thumbnail'])) {
1286            return $this->fields['thumbnail'];
1287        }
1288        $arr = [
1289            'author'     => mb_substr($this->getPrimaryAuthor(), 0, 300, 'utf-8'),
1290            'callnumber' => $this->getCallNumber(),
1291            'size'       => $size,
1292            'title'      => mb_substr($this->getTitle(), 0, 300, 'utf-8'),
1293            'recordid'   => $this->getUniqueID(),
1294            'source'   => $this->getSourceIdentifier(),
1295        ];
1296        $isbns = $this->getCleanISBNs();
1297        if (!empty($isbns)) {
1298            $arr['isbns'] = $isbns;
1299        }
1300        if ($issn = $this->getCleanISSN()) {
1301            $arr['issn'] = $issn;
1302        }
1303        if ($oclc = $this->getCleanOCLCNum()) {
1304            $arr['oclc'] = $oclc;
1305        }
1306        if ($upc = $this->getCleanUPC()) {
1307            $arr['upc'] = $upc;
1308        }
1309        if ($nbn = $this->getCleanNBN()) {
1310            $arr['nbn'] = $nbn['nbn'];
1311        }
1312        if ($ismn = $this->getCleanISMN()) {
1313            $arr['ismn'] = $ismn;
1314        }
1315        if ($uuid = $this->getCleanUuid()) {
1316            $arr['uuid'] = $uuid;
1317        }
1318
1319        // If an ILS driver has injected extra details, check for IDs in there
1320        // to fill gaps:
1321        if ($ilsDetails = $this->getExtraDetail('ils_details')) {
1322            $idTypes = ['isbn', 'issn', 'oclc', 'upc', 'nbn', 'ismn', 'uuid'];
1323            foreach ($idTypes as $key) {
1324                if (!isset($arr[$key]) && isset($ilsDetails[$key])) {
1325                    $arr[$key] = $ilsDetails[$key];
1326                }
1327            }
1328        }
1329        return $arr;
1330    }
1331
1332    /**
1333     * Get the full title of the record.
1334     *
1335     * @return string
1336     */
1337    public function getTitle()
1338    {
1339        return $this->fields['title'] ?? '';
1340    }
1341
1342    /**
1343     * Get the text of the part/section portion of the title.
1344     *
1345     * @return string
1346     */
1347    public function getTitleSection()
1348    {
1349        // Not currently stored in the default index schema
1350        return null;
1351    }
1352
1353    /**
1354     * Get the statement of responsibility that goes with the title (i.e. "by John
1355     * Smith").
1356     *
1357     * @return string
1358     */
1359    public function getTitleStatement()
1360    {
1361        // Not currently stored in the default index schema
1362        return null;
1363    }
1364
1365    /**
1366     * Get an array of lines from the table of contents.
1367     *
1368     * @return array
1369     */
1370    public function getTOC()
1371    {
1372        return (array)($this->fields['contents'] ?? []);
1373    }
1374
1375    /**
1376     * Get hierarchical place names
1377     *
1378     * @return array
1379     */
1380    public function getHierarchicalPlaceNames()
1381    {
1382        // Not currently stored in the default index schema
1383        return [];
1384    }
1385
1386    /**
1387     * Get the UPC number(s) of the record.
1388     *
1389     * @return array
1390     */
1391    public function getUPC()
1392    {
1393        return (array)($this->fields['upc_str_mv'] ?? []);
1394    }
1395
1396    /**
1397     * Get UUIDs (Universally unique identifier). These are commonly used in, for
1398     * example, digital library or repository systems and can be a useful match
1399     * point with third party systems.
1400     *
1401     * @return array
1402     */
1403    public function getUuids()
1404    {
1405        return (array)($this->fields['uuid_str_mv'] ?? []);
1406    }
1407
1408    /**
1409     * Get just the first listed UUID (Universally unique identifier), or false if
1410     * none available.
1411     *
1412     * @return mixed
1413     */
1414    public function getCleanUuid()
1415    {
1416        $uuids = $this->getUuids();
1417        return empty($uuids) ? false : $uuids[0];
1418    }
1419
1420    /**
1421     * Return an array of associative URL arrays with one or more of the following
1422     * keys:
1423     *
1424     * <li>
1425     *   <ul>desc: URL description text to display (optional)</ul>
1426     *   <ul>url: fully-formed URL (required if 'route' is absent)</ul>
1427     *   <ul>route: VuFind route to build URL with (required if 'url' is absent)</ul>
1428     *   <ul>routeParams: Parameters for route (optional)</ul>
1429     *   <ul>queryString: Query params to append after building route (optional)</ul>
1430     * </li>
1431     *
1432     * @return array
1433     */
1434    public function getURLs()
1435    {
1436        $filter = function ($url) {
1437            return ['url' => $url];
1438        };
1439        return array_map($filter, (array)($this->fields['url'] ?? []));
1440    }
1441
1442    /**
1443     * Get the hierarchy_top_id(s) associated with this item (empty if none).
1444     *
1445     * @return array
1446     */
1447    public function getHierarchyTopID()
1448    {
1449        // Unsupported by default:
1450        return [];
1451    }
1452
1453    /**
1454     * Get the absolute parent title(s) associated with this item (empty if none).
1455     *
1456     * @return array
1457     */
1458    public function getHierarchyTopTitle()
1459    {
1460        // Unsupported by default:
1461        return [];
1462    }
1463
1464    /**
1465     * Get an associative array (id => title) of collections containing this record.
1466     *
1467     * @return array
1468     */
1469    public function getContainingCollections()
1470    {
1471        // Unsupported by default:
1472        return [];
1473    }
1474
1475    /**
1476     * Get the value of whether or not this is a collection level record
1477     *
1478     * NOTE: \VuFind\Hierarchy\TreeDataFormatter\AbstractBase::isCollection()
1479     * duplicates some of this logic.
1480     *
1481     * @return bool
1482     */
1483    public function isCollection()
1484    {
1485        // Unsupported by default:
1486        return false;
1487    }
1488
1489    /**
1490     * Get a list of hierarchy trees containing this record.
1491     *
1492     * @param string $hierarchyID The hierarchy to get the tree for
1493     *
1494     * @return mixed An associative array of hierarchy trees on success
1495     * (id => title), false if no hierarchies found
1496     *
1497     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1498     */
1499    public function getHierarchyTrees($hierarchyID = false)
1500    {
1501        // Unsupported by default:
1502        return false;
1503    }
1504
1505    /**
1506     * Get the Hierarchy Type (false if none)
1507     *
1508     * @return string|bool
1509     */
1510    public function getHierarchyType()
1511    {
1512        // Unsupported by default:
1513        return false;
1514    }
1515
1516    /**
1517     * Return the unique identifier of this record within the index;
1518     * useful for retrieving additional information (like tags and user
1519     * comments) from the external MySQL database.
1520     *
1521     * @return string Unique identifier.
1522     */
1523    public function getUniqueID()
1524    {
1525        if (!isset($this->fields['id'])) {
1526            throw new \Exception('ID not set!');
1527        }
1528        return $this->fields['id'];
1529    }
1530
1531    /**
1532     * Return an XML representation of the record using the specified format.
1533     * Return false if the format is unsupported.
1534     *
1535     * @param string       $format  Name of format to use (corresponds with
1536     * OAI-PMH metadataPrefix parameter).
1537     * @param string       $baseUrl Base URL of host containing VuFind (optional;
1538     * may be used to inject record URLs into XML when appropriate).
1539     * @param RecordLinker $linker  Record linker helper (optional; may be used to
1540     * inject record URLs into XML when appropriate).
1541     *
1542     * @return mixed XML, or false if format unsupported.
1543     */
1544    public function getXML($format, $baseUrl = null, $linker = null)
1545    {
1546        // For OAI-PMH Dublin Core, produce the necessary XML:
1547        if ($format == 'oai_dc') {
1548            $dc = 'http://purl.org/dc/elements/1.1/';
1549            $xml = new \SimpleXMLElement(
1550                '<oai_dc:dc '
1551                . 'xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" '
1552                . 'xmlns:dc="' . $dc . '" '
1553                . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
1554                . 'xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ '
1555                . 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd" />'
1556            );
1557            $xml->addChild('title', htmlspecialchars($this->getTitle()), $dc);
1558            $authors = $this->getDeduplicatedAuthors();
1559            foreach ($authors as $list) {
1560                foreach (array_keys($list) as $author) {
1561                    $xml->addChild('creator', htmlspecialchars($author), $dc);
1562                }
1563            }
1564            foreach ($this->getLanguages() as $lang) {
1565                $xml->addChild('language', htmlspecialchars($lang), $dc);
1566            }
1567            foreach ($this->getPublishers() as $pub) {
1568                $xml->addChild('publisher', htmlspecialchars($pub), $dc);
1569            }
1570            foreach ($this->getPublicationDates() as $date) {
1571                $xml->addChild('date', htmlspecialchars($date), $dc);
1572            }
1573            foreach ($this->getAllSubjectHeadings() as $subj) {
1574                $xml->addChild(
1575                    'subject',
1576                    htmlspecialchars(implode(' -- ', $subj)),
1577                    $dc
1578                );
1579            }
1580            if (null !== $baseUrl && null !== $linker) {
1581                $url = $baseUrl . $linker->getUrl($this);
1582                $xml->addChild('identifier', $url, $dc);
1583            }
1584
1585            return $xml->asXml();
1586        }
1587
1588        // Unsupported format:
1589        return false;
1590    }
1591
1592    /**
1593     * Get an array of strings representing citation formats supported
1594     * by this record's data (empty if none). For possible legal values,
1595     * see /application/themes/root/helpers/Citation.php, getCitation()
1596     * method.
1597     *
1598     * @return array Strings representing citation formats.
1599     */
1600    protected function getSupportedCitationFormats()
1601    {
1602        return ['APA', 'Chicago', 'MLA'];
1603    }
1604
1605    /**
1606     * Get the title of the item that contains this record (i.e. MARC 773s of a
1607     * journal).
1608     *
1609     * @return string
1610     */
1611    public function getContainerTitle()
1612    {
1613        return $this->fields['container_title'] ?? '';
1614    }
1615
1616    /**
1617     * Get the volume of the item that contains this record (i.e. MARC 773v of a
1618     * journal).
1619     *
1620     * @return string
1621     */
1622    public function getContainerVolume()
1623    {
1624        return $this->fields['container_volume'] ?? '';
1625    }
1626
1627    /**
1628     * Get the issue of the item that contains this record (i.e. MARC 773l of a
1629     * journal).
1630     *
1631     * @return string
1632     */
1633    public function getContainerIssue()
1634    {
1635        return $this->fields['container_issue'] ?? '';
1636    }
1637
1638    /**
1639     * Get the start page of the item that contains this record (i.e. MARC 773q of a
1640     * journal).
1641     *
1642     * @return string
1643     */
1644    public function getContainerStartPage()
1645    {
1646        return $this->fields['container_start_page'] ?? '';
1647    }
1648
1649    /**
1650     * Get the end page of the item that contains this record.
1651     *
1652     * @return string
1653     */
1654    public function getContainerEndPage()
1655    {
1656        // Not supported by the default index schema -- implement in child classes.
1657        return '';
1658    }
1659
1660    /**
1661     * Get a full, free-form reference to the context of the item that contains this
1662     * record (i.e. volume, year, issue, pages).
1663     *
1664     * @return string
1665     */
1666    public function getContainerReference()
1667    {
1668        return $this->fields['container_reference'] ?? '';
1669    }
1670
1671    /**
1672     * Get a sortable title for the record (i.e. no leading articles).
1673     *
1674     * @return string
1675     */
1676    public function getSortTitle()
1677    {
1678        return $this->fields['title_sort'] ?? parent::getSortTitle();
1679    }
1680
1681    /**
1682     * Get schema.org type mapping, an array of sub-types of
1683     * http://schema.org/CreativeWork, defaulting to CreativeWork
1684     * itself if nothing else matches.
1685     *
1686     * @return array
1687     */
1688    public function getSchemaOrgFormatsArray()
1689    {
1690        $types = [];
1691        foreach ($this->getFormats() as $format) {
1692            switch ($format) {
1693                case 'Book':
1694                case 'eBook':
1695                    $types['Book'] = 1;
1696                    break;
1697                case 'Video':
1698                case 'VHS':
1699                    $types['Movie'] = 1;
1700                    break;
1701                case 'Photo':
1702                    $types['Photograph'] = 1;
1703                    break;
1704                case 'Map':
1705                    $types['Map'] = 1;
1706                    break;
1707                case 'Audio':
1708                    $types['MusicAlbum'] = 1;
1709                    break;
1710                default:
1711                    $types['CreativeWork'] = 1;
1712            }
1713        }
1714        return array_keys($types);
1715    }
1716
1717    /**
1718     * Get schema.org type mapping, expected to be a space-delimited string of
1719     * sub-types of http://schema.org/CreativeWork, defaulting to CreativeWork
1720     * itself if nothing else matches.
1721     *
1722     * @return string
1723     */
1724    public function getSchemaOrgFormats()
1725    {
1726        return implode(' ', $this->getSchemaOrgFormatsArray());
1727    }
1728
1729    /**
1730     * Get information on records deduplicated with this one
1731     *
1732     * @return array Array keyed by source id containing record id
1733     */
1734    public function getDedupData()
1735    {
1736        return $this->fields['dedup_data'] ?? [];
1737    }
1738
1739    /**
1740     * Get the number of child records belonging to this record
1741     *
1742     * @return int Number of records
1743     */
1744    public function getChildRecordCount()
1745    {
1746        // Unsupported by default
1747        return 0;
1748    }
1749
1750    /**
1751     * Get the container record id.
1752     *
1753     * @return string Container record id (empty string if none)
1754     */
1755    public function getContainerRecordID()
1756    {
1757        // Unsupported by default
1758        return '';
1759    }
1760
1761    /**
1762     * Get the bbox-geo variable.
1763     *
1764     * @return array
1765     */
1766    public function getGeoLocation()
1767    {
1768        return (array)($this->fields['long_lat'] ?? []);
1769    }
1770
1771    /**
1772     * Get the map display (lat/lon) coordinates
1773     *
1774     * @return array
1775     */
1776    public function getDisplayCoordinates()
1777    {
1778        return (array)($this->fields['long_lat_display'] ?? []);
1779    }
1780
1781    /**
1782     * Get the map display (lat/lon) labels
1783     *
1784     * @return array
1785     */
1786    public function getCoordinateLabels()
1787    {
1788        return (array)($this->fields['long_lat_label'] ?? []);
1789    }
1790}