Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
76.34% |
242 / 317 |
|
73.33% |
33 / 45 |
CRAP | |
0.00% |
0 / 1 |
MarcAdvancedTrait | |
76.34% |
242 / 317 |
|
73.33% |
33 / 45 |
423.45 | |
0.00% |
0 / 1 |
getAccessRestrictions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllSubjectHeadings | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getAllSubjectHeadingsRecordOrder | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
getAllSubjectHeadingsNumericalOrder | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
processSubjectHeadings | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
5.01 | |||
getAwards | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBibliographicLevel | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
90 | |||
getBibliographyNotes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFilteredXML | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFindingAids | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGeneralNotes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHumanReadablePublicationDates | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNewerTitles | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getPlacesOfPublication | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPlayingTimes | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getPreviousTitles | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getProductionCredits | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPublicationFrequency | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRelationshipNotes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSeries | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
3.65 | |||
getSeriesFromMARC | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
getSummary | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSystemDetails | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTargetAudienceNotes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTitleSection | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTitleStatement | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTOC | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
getHierarchicalPlaceNames | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
getURLs | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
7 | |||
getAllRecordLinks | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
8.01 | |||
getRecordLinkNote | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
9.00 | |||
getFieldData | |
42.55% |
20 / 47 |
|
0.00% |
0 / 1 |
87.44 | |||
getIdFromLinkingField | |
37.50% |
3 / 8 |
|
0.00% |
0 / 1 |
11.10 | |||
extractSingleMarcDetail | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getFormattedMarcDetails | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
6 | |||
getXML | |
80.95% |
17 / 21 |
|
0.00% |
0 / 1 |
5.17 | |||
getRDFXML | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
2.09 | |||
getConsortialIDs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCleanISMN | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getCleanNBN | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
getTitlesAltScript | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getFullTitlesAltScript | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getShortTitlesAltScript | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSubtitlesAltScript | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTitleSectionsAltScript | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * Functions to add advanced MARC-driven functionality to a record driver already |
5 | * powered by the standard index spec. Depends upon MarcReaderTrait. |
6 | * |
7 | * PHP version 8 |
8 | * |
9 | * Copyright (C) Villanova University 2017. |
10 | * Copyright (C) The National Library of Finland 2020. |
11 | * |
12 | * This program is free software; you can redistribute it and/or modify |
13 | * it under the terms of the GNU General Public License version 2, |
14 | * as published by the Free Software Foundation. |
15 | * |
16 | * This program is distributed in the hope that it will be useful, |
17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
19 | * GNU General Public License for more details. |
20 | * |
21 | * You should have received a copy of the GNU General Public License |
22 | * along with this program; if not, write to the Free Software |
23 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
24 | * |
25 | * @category VuFind |
26 | * @package RecordDrivers |
27 | * @author Demian Katz <demian.katz@villanova.edu> |
28 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
29 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
30 | * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki |
31 | */ |
32 | |
33 | namespace VuFind\RecordDriver\Feature; |
34 | |
35 | use VuFind\View\Helper\Root\RecordLinker; |
36 | use VuFind\XSLT\Processor as XSLTProcessor; |
37 | |
38 | use function count; |
39 | use function in_array; |
40 | use function is_array; |
41 | |
42 | /** |
43 | * Functions to add advanced MARC-driven functionality to a record driver already |
44 | * powered by the standard index spec. Depends upon MarcReaderTrait. |
45 | * |
46 | * @category VuFind |
47 | * @package RecordDrivers |
48 | * @author Demian Katz <demian.katz@villanova.edu> |
49 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
50 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
51 | * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki |
52 | */ |
53 | trait MarcAdvancedTrait |
54 | { |
55 | /** |
56 | * Fields that may contain subject headings, and their descriptions |
57 | * |
58 | * @var array |
59 | */ |
60 | protected $subjectFields = [ |
61 | '600' => 'personal name', |
62 | '610' => 'corporate name', |
63 | '611' => 'meeting name', |
64 | '630' => 'uniform title', |
65 | '648' => 'chronological', |
66 | '650' => 'topic', |
67 | '651' => 'geographic', |
68 | '653' => '', |
69 | '655' => 'genre/form', |
70 | '656' => 'occupation', |
71 | ]; |
72 | |
73 | /** |
74 | * Mappings from subject source indicators (2nd indicator of subject fields in |
75 | * MARC 21) to the their codes. |
76 | * |
77 | * @var array |
78 | * @link https://www.loc.gov/marc/bibliographic/bd6xx.html Subject field docs |
79 | * @link https://www.loc.gov/standards/sourcelist/subject.html Code list |
80 | */ |
81 | protected $subjectSources = [ |
82 | '0' => 'lcsh', |
83 | '1' => 'lcshac', |
84 | '2' => 'mesh', |
85 | '3' => 'nal', |
86 | '4' => 'unknown', |
87 | '5' => 'cash', |
88 | '6' => 'rvm', |
89 | ]; |
90 | |
91 | /** |
92 | * Type to export in getXML(). |
93 | * |
94 | * @var string |
95 | */ |
96 | protected $xmlType = 'Bibliographic'; |
97 | |
98 | /** |
99 | * Get access restriction notes for the record. |
100 | * |
101 | * @return array |
102 | */ |
103 | public function getAccessRestrictions() |
104 | { |
105 | return $this->getFieldArray('506'); |
106 | } |
107 | |
108 | /** |
109 | * Get all subject headings associated with this record. Each heading is |
110 | * returned as an array of chunks, increasing from least specific to most |
111 | * specific. |
112 | * |
113 | * @param bool $extended Whether to return a keyed array with the following |
114 | * keys: |
115 | * - heading: the actual subject heading chunks |
116 | * - type: heading type |
117 | * - source: source vocabulary |
118 | * |
119 | * @return array |
120 | */ |
121 | public function getAllSubjectHeadings($extended = false) |
122 | { |
123 | if (($this->mainConfig->Record->marcSubjectHeadingsSort ?? '') === 'numerical') { |
124 | $returnValues = $this->getAllSubjectHeadingsNumericalOrder($extended); |
125 | } else { |
126 | // Default | value === 'record' |
127 | $returnValues = $this->getAllSubjectHeadingsRecordOrder($extended); |
128 | } |
129 | |
130 | // Remove duplicates and then send back everything we collected: |
131 | return array_map( |
132 | 'unserialize', |
133 | array_unique(array_map('serialize', $returnValues)) |
134 | ); |
135 | } |
136 | |
137 | /** |
138 | * Get all subject headings associated with this record. Each heading is |
139 | * returned as an array of chunks, increasing from least specific to most |
140 | * specific. Sorted in the same way it is saved for the record. |
141 | * |
142 | * @param bool $extended Whether to return a keyed array with the following |
143 | * keys: |
144 | * - heading: the actual subject heading chunks |
145 | * - type: heading type |
146 | * - source: source vocabulary |
147 | * |
148 | * @return array |
149 | */ |
150 | protected function getAllSubjectHeadingsRecordOrder(bool $extended = false): array |
151 | { |
152 | $returnValues = []; |
153 | $allFields = $this->getMarcReader()->getAllFields(); |
154 | $subjectFieldsKeys = array_keys($this->subjectFields); |
155 | // Go through all the fields and handle them if they are part of what we want |
156 | foreach ($allFields as $field) { |
157 | if (isset($field['tag']) && in_array($field['tag'], $subjectFieldsKeys)) { |
158 | $fieldType = $this->subjectFields[$field['tag']]; |
159 | if ($nextLine = $this->processSubjectHeadings($field, $extended, $fieldType)) { |
160 | $returnValues[] = $nextLine; |
161 | } |
162 | } |
163 | } |
164 | return $returnValues; |
165 | } |
166 | |
167 | /** |
168 | * Get all subject headings associated with this record. Each heading is |
169 | * returned as an array of chunks, increasing from least specific to most |
170 | * specific. Sorted numerically on marc fields. |
171 | * |
172 | * @param bool $extended Whether to return a keyed array with the following |
173 | * keys: |
174 | * - heading: the actual subject heading chunks |
175 | * - type: heading type |
176 | * - source: source vocabulary |
177 | * |
178 | * @return array |
179 | */ |
180 | protected function getAllSubjectHeadingsNumericalOrder(bool $extended = false): array |
181 | { |
182 | $returnValues = []; |
183 | // Try each MARC field one at a time: |
184 | foreach ($this->subjectFields as $field => $fieldType) { |
185 | // Do we have any results for the current field? If not, try the next. |
186 | $fields = $this->getMarcReader()->getFields($field); |
187 | if (!$fields) { |
188 | continue; |
189 | } |
190 | |
191 | // If we got here, we found results -- let's loop through them. |
192 | foreach ($fields as $f) { |
193 | if ($nextLine = $this->processSubjectHeadings($f, $extended, $fieldType)) { |
194 | $returnValues[] = $nextLine; |
195 | } |
196 | } |
197 | } |
198 | return $returnValues; |
199 | } |
200 | |
201 | /** |
202 | * Get subject headings of a given record field. |
203 | * The heading is returned as a chunk, increasing from least specific to most specific. |
204 | * |
205 | * @param array $field field to handle |
206 | * @param bool $extended Whether to return a keyed array with the following keys: |
207 | * - heading: the actual subject heading chunks - type: |
208 | * heading type - source: source vocabulary |
209 | * @param string $fieldType Type of the field |
210 | * |
211 | * @return ?array |
212 | */ |
213 | protected function processSubjectHeadings( |
214 | array $field, |
215 | bool $extended, |
216 | string $fieldType |
217 | ): ?array { |
218 | // Start an array for holding the chunks of the current heading: |
219 | $current = []; |
220 | |
221 | // Get all the chunks and collect them together: |
222 | foreach ($field['subfields'] as $subfield) { |
223 | // Numeric subfields are for control purposes and should not |
224 | // be displayed: |
225 | if (!is_numeric($subfield['code'])) { |
226 | $current[] = $subfield['data']; |
227 | } |
228 | } |
229 | // If we found at least one chunk, add a heading to our result: |
230 | if (!empty($current)) { |
231 | if ($extended) { |
232 | $sourceIndicator = $field['i2']; |
233 | $source = $this->subjectSources[$sourceIndicator] |
234 | ?? $this->getSubfield($field, '2'); |
235 | return [ |
236 | 'heading' => $current, |
237 | 'type' => $fieldType, |
238 | 'source' => $source, |
239 | 'id' => $this->getSubfield($field, '0'), |
240 | ]; |
241 | } else { |
242 | return $current; |
243 | } |
244 | } |
245 | return null; |
246 | } |
247 | |
248 | /** |
249 | * Get award notes for the record. |
250 | * |
251 | * @return array |
252 | */ |
253 | public function getAwards() |
254 | { |
255 | return $this->getFieldArray('586'); |
256 | } |
257 | |
258 | /** |
259 | * Get the bibliographic level of the current record. |
260 | * |
261 | * @return string |
262 | */ |
263 | public function getBibliographicLevel() |
264 | { |
265 | $leader = $this->getMarcReader()->getLeader(); |
266 | $biblioLevel = strtoupper($leader[7]); |
267 | |
268 | switch ($biblioLevel) { |
269 | case 'M': // Monograph |
270 | return 'Monograph'; |
271 | case 'S': // Serial |
272 | return 'Serial'; |
273 | case 'A': // Monograph Part |
274 | return 'MonographPart'; |
275 | case 'B': // Serial Part |
276 | return 'SerialPart'; |
277 | case 'C': // Collection |
278 | return 'Collection'; |
279 | case 'D': // Collection Part |
280 | return 'CollectionPart'; |
281 | case 'I': // Integrating Resource |
282 | return 'IntegratingResource'; |
283 | default: |
284 | return 'Unknown'; |
285 | } |
286 | } |
287 | |
288 | /** |
289 | * Get notes on bibliography content. |
290 | * |
291 | * @return array |
292 | */ |
293 | public function getBibliographyNotes() |
294 | { |
295 | return $this->getFieldArray('504'); |
296 | } |
297 | |
298 | /** |
299 | * Return full record as filtered XML for public APIs. |
300 | * |
301 | * @return string |
302 | */ |
303 | public function getFilteredXML() |
304 | { |
305 | // The default implementation does not filter out any fields. You can do |
306 | // simple filtering using MarcReader's getFilteredRecord method, or more |
307 | // complex changes by using the XML DOM. |
308 | // |
309 | // Example for removing field 520, 9xx fields and subfield 0 from all fields |
310 | // with getFilteredRecord: |
311 | // |
312 | // |
313 | // return $this->getMarcReader()->getFilteredRecord( |
314 | // [ |
315 | // [ |
316 | // 'tag' => '520', |
317 | // ], |
318 | // [ |
319 | // 'tag' => '9..', |
320 | // ], |
321 | // [ |
322 | // 'tag' => '...', |
323 | // 'subfields' => '0', |
324 | // ], |
325 | // ] |
326 | // )->toFormat('MARCXML'); |
327 | // |
328 | // |
329 | // Example for removing field 520 using DOM (note that the fields must be |
330 | // removed in a second loop to not affect the iteration of fields) and adding |
331 | // a new 955 field: |
332 | // |
333 | // $collection = new \DOMDocument(); |
334 | // $collection->preserveWhiteSpace = false; |
335 | // $collection->loadXML($this->getMarcReader()->toFormat('MARCXML')); |
336 | // $record = $collection->getElementsByTagName('record')->item(0); |
337 | // $fieldsToRemove = []; |
338 | // foreach ($record->getElementsByTagName('datafield') as $field) { |
339 | // $tag = $field->getAttribute('tag'); |
340 | // if ('520' === $tag) { |
341 | // $fieldsToRemove[] = $field; |
342 | // } |
343 | // } |
344 | // foreach ($fieldsToRemove as $field) { |
345 | // $record->removeChild($field); |
346 | // } |
347 | // |
348 | // $field = $collection->createElement('datafield'); |
349 | // $tag = $collection->createAttribute('tag'); |
350 | // $tag->value = '955'; |
351 | // $field->appendChild($tag); |
352 | // $ind1 = $collection->createAttribute('ind1'); |
353 | // $ind1->value = ' '; |
354 | // $field->appendChild($ind1); |
355 | // $ind2 = $collection->createAttribute('ind2'); |
356 | // $ind2->value = ' '; |
357 | // $field->appendChild($ind2); |
358 | // $subfield = $collection->createElement('subfield'); |
359 | // $code = $collection->createAttribute('code'); |
360 | // $code->value = 'a'; |
361 | // $subfield->appendChild($code); |
362 | // $subfield->appendChild($collection->createTextNode('VuFind')); |
363 | // $field->appendChild($subfield); |
364 | // $record->appendChild($field); |
365 | // |
366 | // return $collection->saveXML(); |
367 | |
368 | return $this->getMarcReader()->toFormat('MARCXML'); |
369 | } |
370 | |
371 | /** |
372 | * Get notes on finding aids related to the record. |
373 | * |
374 | * @return array |
375 | */ |
376 | public function getFindingAids() |
377 | { |
378 | return $this->getFieldArray('555'); |
379 | } |
380 | |
381 | /** |
382 | * Get general notes on the record. |
383 | * |
384 | * @return array |
385 | */ |
386 | public function getGeneralNotes() |
387 | { |
388 | return $this->getFieldArray('500'); |
389 | } |
390 | |
391 | /** |
392 | * Get human readable publication dates for display purposes (may not be suitable |
393 | * for computer processing -- use getPublicationDates() for that). |
394 | * |
395 | * @return array |
396 | */ |
397 | public function getHumanReadablePublicationDates() |
398 | { |
399 | return $this->getPublicationInfo('c'); |
400 | } |
401 | |
402 | /** |
403 | * Get an array of newer titles for the record. |
404 | * |
405 | * @return array |
406 | */ |
407 | public function getNewerTitles() |
408 | { |
409 | // If the MARC links are being used, return blank array |
410 | $fieldsNames = isset($this->mainConfig->Record->marc_links) |
411 | ? array_map('trim', explode(',', $this->mainConfig->Record->marc_links)) |
412 | : []; |
413 | return in_array('785', $fieldsNames) ? [] : parent::getNewerTitles(); |
414 | } |
415 | |
416 | /** |
417 | * Get the item's places of publication. |
418 | * |
419 | * @return array |
420 | */ |
421 | public function getPlacesOfPublication() |
422 | { |
423 | return $this->getPublicationInfo(); |
424 | } |
425 | |
426 | /** |
427 | * Get an array of playing times for the record (if applicable). |
428 | * |
429 | * @return array |
430 | */ |
431 | public function getPlayingTimes() |
432 | { |
433 | $times = $this->getFieldArray('306', ['a'], false); |
434 | |
435 | // Format the times to include colons ("HH:MM:SS" format). |
436 | foreach ($times as $x => $time) { |
437 | if (!preg_match('/\d\d:\d\d:\d\d/', $time)) { |
438 | $times[$x] = substr($time, 0, 2) . ':' . |
439 | substr($time, 2, 2) . ':' . |
440 | substr($time, 4, 2); |
441 | } |
442 | } |
443 | return $times; |
444 | } |
445 | |
446 | /** |
447 | * Get an array of previous titles for the record. |
448 | * |
449 | * @return array |
450 | */ |
451 | public function getPreviousTitles() |
452 | { |
453 | // If the MARC links are being used, return blank array |
454 | $fieldsNames = isset($this->mainConfig->Record->marc_links) |
455 | ? array_map('trim', explode(',', $this->mainConfig->Record->marc_links)) |
456 | : []; |
457 | return in_array('780', $fieldsNames) ? [] : parent::getPreviousTitles(); |
458 | } |
459 | |
460 | /** |
461 | * Get credits of people involved in production of the item. |
462 | * |
463 | * @return array |
464 | */ |
465 | public function getProductionCredits() |
466 | { |
467 | return $this->getFieldArray('508'); |
468 | } |
469 | |
470 | /** |
471 | * Get an array of publication frequency information. |
472 | * |
473 | * @return array |
474 | */ |
475 | public function getPublicationFrequency() |
476 | { |
477 | return $this->getFieldArray('310', ['a', 'b']); |
478 | } |
479 | |
480 | /** |
481 | * Get an array of strings describing relationships to other items. |
482 | * |
483 | * @return array |
484 | */ |
485 | public function getRelationshipNotes() |
486 | { |
487 | return $this->getFieldArray('580'); |
488 | } |
489 | |
490 | /** |
491 | * Get an array of all series names containing the record. Array entries may |
492 | * be either the name string, or an associative array with 'name' and 'number' |
493 | * keys. |
494 | * |
495 | * @return array |
496 | */ |
497 | public function getSeries() |
498 | { |
499 | // First check the 440, 800 and 830 fields for series information: |
500 | $primaryFields = [ |
501 | '440' => ['a', 'p'], |
502 | '800' => ['a', 'b', 'c', 'd', 'f', 'p', 'q', 't'], |
503 | '830' => ['a', 'p']]; |
504 | $matches = $this->getSeriesFromMARC($primaryFields); |
505 | if (!empty($matches)) { |
506 | return $matches; |
507 | } |
508 | |
509 | // Now check 490 and display it only if 440/800/830 were empty: |
510 | $secondaryFields = ['490' => ['a']]; |
511 | $matches = $this->getSeriesFromMARC($secondaryFields); |
512 | if (!empty($matches)) { |
513 | return $matches; |
514 | } |
515 | |
516 | // Still no results found? Resort to the Solr-based method just in case! |
517 | return parent::getSeries(); |
518 | } |
519 | |
520 | /** |
521 | * Support method for getSeries() -- given a field specification, look for |
522 | * series information in the MARC record. |
523 | * |
524 | * @param array $fieldInfo Associative array of field => subfield information |
525 | * (used to find series name) |
526 | * |
527 | * @return array |
528 | */ |
529 | protected function getSeriesFromMARC($fieldInfo) |
530 | { |
531 | $matches = []; |
532 | |
533 | // Loop through the field specification.... |
534 | foreach ($fieldInfo as $field => $subfields) { |
535 | // Did we find any matching fields? |
536 | $series = $this->getMarcReader()->getFields($field); |
537 | foreach ($series as $currentField) { |
538 | // Can we find a name using the specified subfield list? |
539 | $name = $this->getSubfieldArray($currentField, $subfields); |
540 | if (isset($name[0])) { |
541 | $currentArray = ['name' => $name[0]]; |
542 | |
543 | // Can we find a number in subfield v? (Note that number is |
544 | // always in subfield v regardless of whether we are dealing |
545 | // with 440, 490, 800 or 830 -- hence the hard-coded array |
546 | // rather than another parameter in $fieldInfo). |
547 | $number = $this->getSubfieldArray($currentField, ['v']); |
548 | if (isset($number[0])) { |
549 | $currentArray['number'] = $number[0]; |
550 | } |
551 | |
552 | // Save the current match: |
553 | $matches[] = $currentArray; |
554 | } |
555 | } |
556 | } |
557 | |
558 | return $matches; |
559 | } |
560 | |
561 | /** |
562 | * Get an array of summary strings for the record. |
563 | * |
564 | * @return array |
565 | */ |
566 | public function getSummary() |
567 | { |
568 | return $this->getFieldArray('520'); |
569 | } |
570 | |
571 | /** |
572 | * Get an array of technical details on the item represented by the record. |
573 | * |
574 | * @return array |
575 | */ |
576 | public function getSystemDetails() |
577 | { |
578 | return $this->getFieldArray('538'); |
579 | } |
580 | |
581 | /** |
582 | * Get an array of note about the record's target audience. |
583 | * |
584 | * @return array |
585 | */ |
586 | public function getTargetAudienceNotes() |
587 | { |
588 | return $this->getFieldArray('521'); |
589 | } |
590 | |
591 | /** |
592 | * Get the text of the part/section portion of the title. |
593 | * |
594 | * @return string |
595 | */ |
596 | public function getTitleSection() |
597 | { |
598 | return $this->getFirstFieldValue('245', ['n', 'p']); |
599 | } |
600 | |
601 | /** |
602 | * Get the statement of responsibility that goes with the title (i.e. "by John |
603 | * Smith"). |
604 | * |
605 | * @return string |
606 | */ |
607 | public function getTitleStatement() |
608 | { |
609 | return $this->getFirstFieldValue('245', ['c']); |
610 | } |
611 | |
612 | /** |
613 | * Get an array of lines from the table of contents. |
614 | * |
615 | * @return array |
616 | */ |
617 | public function getTOC() |
618 | { |
619 | $toc = []; |
620 | if ( |
621 | $fields = $this->getMarcReader()->getFields( |
622 | '505', |
623 | ['a', 'g', 'r', 't', 'u'] |
624 | ) |
625 | ) { |
626 | foreach ($fields as $field) { |
627 | // Implode all the subfields into a single string, then explode |
628 | // on the -- separators (filtering out empty chunks). Due to |
629 | // inconsistent application of subfield codes, this is the most |
630 | // reliable way to split up a table of contents. |
631 | $str = ''; |
632 | foreach ($field['subfields'] as $subfield) { |
633 | $str .= trim($subfield['data']) . ' '; |
634 | } |
635 | $toc = array_merge( |
636 | $toc, |
637 | array_filter(array_map('trim', preg_split('/[.\s]--/', $str))) |
638 | ); |
639 | } |
640 | } |
641 | return $toc; |
642 | } |
643 | |
644 | /** |
645 | * Get hierarchical place names (MARC field 752) |
646 | * |
647 | * Returns an array of formatted hierarchical place names, consisting of all |
648 | * alpha-subfields, concatenated for display |
649 | * |
650 | * @return array |
651 | */ |
652 | public function getHierarchicalPlaceNames() |
653 | { |
654 | $placeNames = []; |
655 | if ($fields = $this->getMarcReader()->getFields('752')) { |
656 | foreach ($fields as $field) { |
657 | $current = []; |
658 | foreach ($field['subfields'] as $subfield) { |
659 | if (!is_numeric($subfield['code'])) { |
660 | $current[] = $subfield['data']; |
661 | } |
662 | } |
663 | $placeNames[] = implode(' -- ', $current); |
664 | } |
665 | } |
666 | return $placeNames; |
667 | } |
668 | |
669 | /** |
670 | * Return an array of associative URL arrays with one or more of the following |
671 | * keys: |
672 | * |
673 | * <li> |
674 | * <ul>desc: URL description text to display (optional)</ul> |
675 | * <ul>url: fully-formed URL (required if 'route' is absent)</ul> |
676 | * <ul>route: VuFind route to build URL with (required if 'url' is absent)</ul> |
677 | * <ul>routeParams: Parameters for route (optional)</ul> |
678 | * <ul>queryString: Query params to append after building route (optional)</ul> |
679 | * </li> |
680 | * |
681 | * @return array |
682 | */ |
683 | public function getURLs() |
684 | { |
685 | $retVal = []; |
686 | |
687 | // Which fields/subfields should we check for URLs? |
688 | $fieldsToCheck = [ |
689 | '856' => ['y', 'z', '3'], // Standard URL |
690 | '555' => ['a'], // Cumulative index/finding aids |
691 | ]; |
692 | |
693 | foreach ($fieldsToCheck as $field => $subfields) { |
694 | $urls = $this->getMarcReader()->getFields($field); |
695 | foreach ($urls as $url) { |
696 | // Is there an address in the current field? |
697 | $address = $this->getSubfield($url, 'u'); |
698 | if ($address) { |
699 | // Is there a description? If not, just use the URL itself. |
700 | foreach ($subfields as $current) { |
701 | $desc = $this->getSubfield($url, $current); |
702 | if ($desc) { |
703 | break; |
704 | } |
705 | } |
706 | |
707 | $retVal[] = ['url' => $address, 'desc' => $desc ?: $address]; |
708 | } |
709 | } |
710 | } |
711 | |
712 | return $retVal; |
713 | } |
714 | |
715 | /** |
716 | * Get all record links related to the current record. Each link is returned as |
717 | * array. |
718 | * Format: |
719 | * array( |
720 | * array( |
721 | * 'title' => label_for_title |
722 | * 'value' => link_name |
723 | * 'link' => link_URI |
724 | * ), |
725 | * ... |
726 | * ) |
727 | * |
728 | * @return null|array |
729 | */ |
730 | public function getAllRecordLinks() |
731 | { |
732 | // Load configurations: |
733 | $fieldsNames = isset($this->mainConfig->Record->marc_links) |
734 | ? explode(',', $this->mainConfig->Record->marc_links) : []; |
735 | $useVisibilityIndicator |
736 | = $this->mainConfig->Record->marc_links_use_visibility_indicator ?? true; |
737 | |
738 | $retVal = []; |
739 | foreach ($fieldsNames as $value) { |
740 | $value = trim($value); |
741 | $fields = $this->getMarcReader()->getFields($value); |
742 | foreach ($fields as $field) { |
743 | // Check to see if we should display at all |
744 | if ($useVisibilityIndicator) { |
745 | $visibilityIndicator = $field['i1']; |
746 | if ($visibilityIndicator == '1') { |
747 | continue; |
748 | } |
749 | } |
750 | |
751 | // Get data for field |
752 | $tmp = $this->getFieldData($field); |
753 | if (is_array($tmp)) { |
754 | $retVal[] = $tmp; |
755 | } |
756 | } |
757 | } |
758 | return empty($retVal) ? null : $retVal; |
759 | } |
760 | |
761 | /** |
762 | * Support method for getFieldData() -- factor the relationship indicator |
763 | * into the field number where relevant to generate a note to associate |
764 | * with a record link. |
765 | * |
766 | * @param array $field Field to examine |
767 | * |
768 | * @return string |
769 | */ |
770 | protected function getRecordLinkNote($field) |
771 | { |
772 | // If set, use relationship information from subfield i |
773 | if ($subfieldI = $this->getSubfield($field, 'i')) { |
774 | // VuFind will add a colon to the label, so prevent double colons: |
775 | $data = rtrim($subfieldI, ':'); |
776 | if (!empty($data)) { |
777 | return $data; |
778 | } |
779 | } |
780 | |
781 | // Normalize blank relationship indicator to 0: |
782 | $relationshipIndicator = $field['i2']; |
783 | if ($relationshipIndicator == ' ') { |
784 | $relationshipIndicator = '0'; |
785 | } |
786 | |
787 | // Assign notes based on the relationship type |
788 | $value = $field['tag']; |
789 | switch ($value) { |
790 | case '780': |
791 | if (in_array($relationshipIndicator, range('0', '7'))) { |
792 | $value .= '_' . $relationshipIndicator; |
793 | } |
794 | break; |
795 | case '785': |
796 | if (in_array($relationshipIndicator, range('0', '8'))) { |
797 | $value .= '_' . $relationshipIndicator; |
798 | } |
799 | break; |
800 | } |
801 | |
802 | return 'note_' . $value; |
803 | } |
804 | |
805 | /** |
806 | * Returns the array element for the 'getAllRecordLinks' method |
807 | * |
808 | * @param array $field Field to examine |
809 | * |
810 | * @return array|bool Array on success, boolean false if no valid link could be |
811 | * found in the data. |
812 | */ |
813 | protected function getFieldData($field) |
814 | { |
815 | // Make sure that there is a t field to be displayed: |
816 | if (!($title = $this->getSubfield($field, 't'))) { |
817 | return false; |
818 | } |
819 | |
820 | $linkTypeSetting = $this->mainConfig->Record->marc_links_link_types |
821 | ?? 'id,oclc,dlc,isbn,issn,title'; |
822 | $linkTypes = explode(',', $linkTypeSetting); |
823 | $linkFields = $this->getSubfields($field, 'w'); |
824 | |
825 | // Run through the link types specified in the config. |
826 | // For each type, check field for reference |
827 | // If reference found, exit loop and go straight to end |
828 | // If no reference found, check the next link type instead |
829 | foreach ($linkTypes as $linkType) { |
830 | switch (trim($linkType)) { |
831 | case 'oclc': |
832 | foreach ($linkFields as $current) { |
833 | $oclc = $this->getIdFromLinkingField($current, 'OCoLC'); |
834 | if ($oclc) { |
835 | $link = ['type' => 'oclc', 'value' => $oclc]; |
836 | } |
837 | } |
838 | break; |
839 | case 'dlc': |
840 | foreach ($linkFields as $current) { |
841 | $dlc = $this->getIdFromLinkingField($current, 'DLC', true); |
842 | if ($dlc) { |
843 | $link = ['type' => 'dlc', 'value' => $dlc]; |
844 | } |
845 | } |
846 | break; |
847 | case 'id': |
848 | foreach ($linkFields as $current) { |
849 | if ($bibLink = $this->getIdFromLinkingField($current)) { |
850 | $link = ['type' => 'bib', 'value' => $bibLink]; |
851 | } |
852 | } |
853 | break; |
854 | case 'isbn': |
855 | if ($isbn = $this->getSubfield($field, 'z')) { |
856 | $link = [ |
857 | 'type' => 'isn', 'value' => $isbn, |
858 | 'exclude' => $this->getUniqueId(), |
859 | ]; |
860 | } |
861 | break; |
862 | case 'issn': |
863 | if ($issn = $this->getSubfield($field, 'x')) { |
864 | $link = [ |
865 | 'type' => 'isn', 'value' => $issn, |
866 | 'exclude' => $this->getUniqueId(), |
867 | ]; |
868 | } |
869 | break; |
870 | case 'title': |
871 | $link = ['type' => 'title', 'value' => $title]; |
872 | break; |
873 | } |
874 | // Exit loop if we have a link |
875 | if (isset($link)) { |
876 | break; |
877 | } |
878 | } |
879 | // Make sure we have something to display: |
880 | return !isset($link) ? false : [ |
881 | 'title' => $this->getRecordLinkNote($field), |
882 | 'value' => $title, |
883 | 'link' => $link, |
884 | ]; |
885 | } |
886 | |
887 | /** |
888 | * Returns an id extracted from the identifier subfield passed in |
889 | * |
890 | * @param string $idField MARC subfield containing id information |
891 | * @param string $prefix Prefix to search for in id field |
892 | * @param bool $raw Return raw match, or normalize? |
893 | * |
894 | * @return string|bool ID on success, false on failure |
895 | */ |
896 | protected function getIdFromLinkingField($idField, $prefix = null, $raw = false) |
897 | { |
898 | if (preg_match('/\(([^)]+)\)(.+)/', $idField, $matches)) { |
899 | // If prefix matches, return ID: |
900 | if ($matches[1] == $prefix) { |
901 | // Special case -- LCCN should not be stripped: |
902 | return $raw |
903 | ? $matches[2] |
904 | : trim(str_replace(range('a', 'z'), '', ($matches[2]))); |
905 | } |
906 | } elseif ($prefix == null) { |
907 | // If no prefix was given or found, we presume it is a raw bib record |
908 | return $idField; |
909 | } |
910 | return false; |
911 | } |
912 | |
913 | /** |
914 | * Support method for getFormattedMarcDetails() -- extract a single result |
915 | * |
916 | * @param array $currentField Result from MarcReader::getFields |
917 | * @param array $details Parsed instructions from getFormattedMarcDetails() |
918 | * |
919 | * @return string|bool |
920 | */ |
921 | protected function extractSingleMarcDetail($currentField, $details) |
922 | { |
923 | // Simplest case -- "msg" mode (just return a configured message): |
924 | if ($details['mode'] === 'msg') { |
925 | // Map 'true' and 'false' to boolean equivalents: |
926 | $msgMap = ['true' => true, 'false' => false]; |
927 | return $msgMap[$details['params']] ?? $details['params']; |
928 | } |
929 | |
930 | // Standard case -- "marc" mode (extract subfield data): |
931 | $result = $this->getSubfieldArray( |
932 | $currentField, |
933 | // Default to subfield a if nothing is specified: |
934 | str_split($details['params'] ?? 'a'), |
935 | true |
936 | ); |
937 | return count($result) > 0 ? (string)$result[0] : ''; |
938 | } |
939 | |
940 | /** |
941 | * Get Status/Holdings Information from the internally stored MARC Record |
942 | * (support method used by the NoILS driver). |
943 | * |
944 | * @param string $defaultField The MARC Field to retrieve if $data commands do |
945 | * not request something more specific |
946 | * @param array $data The type of data to retrieve from the MARC field; |
947 | * an array of pipe-delimited commands where the first part determines the data |
948 | * retrieval mode, the second part provides further instructions, and the |
949 | * optional third part provides a field to override $defaultField; supported |
950 | * modes: "msg" (for a hard-coded message) and "marc" (for fetching subfield |
951 | * data) |
952 | * |
953 | * @return array |
954 | */ |
955 | public function getFormattedMarcDetails($defaultField, $data) |
956 | { |
957 | // First, parse the instructions into a more useful format, so we know |
958 | // which fields we're going to have to look up. |
959 | $instructions = []; |
960 | foreach ($data as $key => $rawInstruction) { |
961 | $instructionParts = explode('|', $rawInstruction); |
962 | $instructions[$key] = [ |
963 | 'mode' => $instructionParts[0], |
964 | 'params' => $instructionParts[1] ?? null, |
965 | 'field' => $instructionParts[2] ?? $defaultField, |
966 | ]; |
967 | } |
968 | |
969 | // Now fetch all of the MARC data that we need. |
970 | $getTagCallback = function ($instruction) { |
971 | return $instruction['field']; |
972 | }; |
973 | $fields = []; |
974 | foreach (array_unique(array_map($getTagCallback, $instructions)) as $field) { |
975 | $fields[$field] = $this->getMarcReader()->getFields($field); |
976 | } |
977 | |
978 | // Initialize return array |
979 | $matches = []; |
980 | |
981 | // Process the instructions on the requested data. |
982 | foreach ($instructions as $key => $details) { |
983 | foreach ($fields[$details['field']] as $i => $currentField) { |
984 | if (!isset($matches[$i])) { |
985 | $matches[$i] = ['id' => $this->getUniqueId()]; |
986 | } |
987 | $matches[$i][$key] = $this->extractSingleMarcDetail( |
988 | $currentField, |
989 | $details |
990 | ); |
991 | } |
992 | } |
993 | return $matches; |
994 | } |
995 | |
996 | /** |
997 | * Return an XML representation of the record using the specified format. |
998 | * Return false if the format is unsupported. |
999 | * |
1000 | * @param string $format Name of format to use (corresponds with |
1001 | * OAI-PMH metadataPrefix parameter). |
1002 | * @param string $baseUrl Base URL of host containing VuFind (optional; |
1003 | * may be used to inject record URLs into XML when appropriate). |
1004 | * @param RecordLinker $linker Record linker helper (optional; may be used to |
1005 | * inject record URLs into XML when appropriate). |
1006 | * |
1007 | * @return mixed XML, or false if format unsupported. |
1008 | */ |
1009 | public function getXML($format, $baseUrl = null, $linker = null) |
1010 | { |
1011 | // Special case for MARC: |
1012 | if ($format == 'marc21') { |
1013 | try { |
1014 | $xml = $this->getMarcReader()->toFormat('MARCXML'); |
1015 | } catch (\Exception) { |
1016 | return false; |
1017 | } |
1018 | $xml = simplexml_load_string($xml); |
1019 | if (!$xml || !isset($xml->record)) { |
1020 | return false; |
1021 | } |
1022 | |
1023 | // Set up proper namespacing and extract just the <record> tag: |
1024 | $xml->record->addAttribute('xmlns', 'http://www.loc.gov/MARC21/slim'); |
1025 | // There's a quirk in SimpleXML that strips the first namespace |
1026 | // declaration, hence the double xmlns: prefix: |
1027 | $xml->record->addAttribute( |
1028 | 'xmlns:xmlns:xsi', |
1029 | 'http://www.w3.org/2001/XMLSchema-instance' |
1030 | ); |
1031 | $xml->record->addAttribute( |
1032 | 'xsi:schemaLocation', |
1033 | 'http://www.loc.gov/MARC21/slim ' . |
1034 | 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd', |
1035 | 'http://www.w3.org/2001/XMLSchema-instance' |
1036 | ); |
1037 | $xml->record->addAttribute('type', $this->xmlType); |
1038 | return $xml->record->asXML(); |
1039 | } |
1040 | |
1041 | // Try the parent method: |
1042 | return parent::getXML($format, $baseUrl, $linker); |
1043 | } |
1044 | |
1045 | /** |
1046 | * Get an XML RDF representation of the data in this record. |
1047 | * |
1048 | * @return mixed XML RDF data (empty if unsupported or error). |
1049 | */ |
1050 | public function getRDFXML() |
1051 | { |
1052 | try { |
1053 | $xml = $this->getMarcReader()->toFormat('MARCXML'); |
1054 | } catch (\Exception $e) { |
1055 | return ''; |
1056 | } |
1057 | return XSLTProcessor::process( |
1058 | 'record-rdf-mods.xsl', |
1059 | trim($xml) |
1060 | ); |
1061 | } |
1062 | |
1063 | /** |
1064 | * Return the list of "source records" for this consortial record. |
1065 | * |
1066 | * @return array |
1067 | */ |
1068 | public function getConsortialIDs() |
1069 | { |
1070 | return $this->getFieldArray('035'); |
1071 | } |
1072 | |
1073 | /** |
1074 | * Return first ISMN found for this record, or false if no one found |
1075 | * |
1076 | * @return mixed |
1077 | */ |
1078 | public function getCleanISMN() |
1079 | { |
1080 | $fields024 = $this->getMarcReader()->getFields('024'); |
1081 | foreach ($fields024 as $field) { |
1082 | if ( |
1083 | $field['i1'] == 2 |
1084 | && $subfield = $this->getSubfield($field, 'a') |
1085 | ) { |
1086 | return $subfield; |
1087 | } |
1088 | } |
1089 | return false; |
1090 | } |
1091 | |
1092 | /** |
1093 | * Return first national bibliography number found, or false if not found |
1094 | * |
1095 | * @return mixed |
1096 | */ |
1097 | public function getCleanNBN() |
1098 | { |
1099 | $field = $this->getMarcReader()->getField('015'); |
1100 | if ($field && $nbn = $this->getSubfield($field, 'a')) { |
1101 | $result = compact('nbn'); |
1102 | if ($source = $this->getSubfield($field, '7')) { |
1103 | $result['source'] = $source; |
1104 | } |
1105 | return $result; |
1106 | } |
1107 | return false; |
1108 | } |
1109 | |
1110 | /** |
1111 | * Get the full titles of the record in alternative scripts. |
1112 | * |
1113 | * @return array |
1114 | */ |
1115 | public function getTitlesAltScript(): array |
1116 | { |
1117 | return $this->getMarcReader() |
1118 | ->getLinkedFieldsSubfields('880', '245', ['a', 'b']); |
1119 | } |
1120 | |
1121 | /** |
1122 | * Get the full titles of the record including section and part information in |
1123 | * alternative scripts. |
1124 | * |
1125 | * @return array |
1126 | */ |
1127 | public function getFullTitlesAltScript(): array |
1128 | { |
1129 | return $this->getMarcReader() |
1130 | ->getLinkedFieldsSubfields('880', '245', ['a', 'b', 'n', 'p']); |
1131 | } |
1132 | |
1133 | /** |
1134 | * Get the short (pre-subtitle) title of the record in alternative scripts. |
1135 | * |
1136 | * @return array |
1137 | */ |
1138 | public function getShortTitlesAltScript(): array |
1139 | { |
1140 | return $this->getMarcReader()->getLinkedFieldsSubfields('880', '245', ['a']); |
1141 | } |
1142 | |
1143 | /** |
1144 | * Get the subtitle of the record in alternative script. |
1145 | * |
1146 | * @return array |
1147 | */ |
1148 | public function getSubtitlesAltScript(): array |
1149 | { |
1150 | return $this->getMarcReader()->getLinkedFieldsSubFields('880', '245', ['b']); |
1151 | } |
1152 | |
1153 | /** |
1154 | * Get the text of the part/section portion of the title in alternative scripts. |
1155 | * |
1156 | * @return array |
1157 | */ |
1158 | public function getTitleSectionsAltScript(): array |
1159 | { |
1160 | return $this->getMarcReader() |
1161 | ->getLinkedFieldsSubfields('880', '245', ['n', 'p']); |
1162 | } |
1163 | } |