Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.05% |
117 / 158 |
|
23.53% |
4 / 17 |
CRAP | |
0.00% |
0 / 1 |
SearchRequestModel | |
74.05% |
117 / 158 |
|
23.53% |
4 / 17 |
211.25 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatDateLimiter | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
setParameters | |
80.00% |
12 / 15 |
|
0.00% |
0 / 1 |
10.80 | |||
convertToQueryString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
convertToQueryStringParameterArray | |
84.62% |
33 / 39 |
|
0.00% |
0 / 1 |
26.10 | |||
convertToSearchRequestJSON | |
91.94% |
57 / 62 |
|
0.00% |
0 / 1 |
26.35 | |||
isParameterIndexed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIndexedParameterName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addLimiter | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addExpander | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addfilter | |
66.67% |
8 / 12 |
|
0.00% |
0 / 1 |
4.59 | |||
escapeSpecialCharacters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
escapeSpecialCharactersForActions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__get | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
__set | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * EBSCO EDS API Search Model |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Serials Solutions 2011. |
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 EBSCOIndustries |
24 | * @package EBSCO |
25 | * @author Michelle Milton <mmilton@epnet.com> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org |
28 | */ |
29 | |
30 | namespace VuFindSearch\Backend\EDS; |
31 | |
32 | use function array_key_exists; |
33 | use function count; |
34 | use function intval; |
35 | use function strlen; |
36 | |
37 | /** |
38 | * EBSCO EDS API Search Model |
39 | * |
40 | * @category EBSCOIndustries |
41 | * @package EBSCO |
42 | * @author Michelle Milton <mmilton@epnet.com> |
43 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
44 | * @link https://vufind.org |
45 | */ |
46 | class SearchRequestModel |
47 | { |
48 | /** |
49 | * What to search for, formatted as [{boolean operator},][{field code}:]{term} |
50 | * |
51 | * @var array |
52 | */ |
53 | protected $query = []; |
54 | |
55 | /** |
56 | * Whether or not to return facets with the search results. valid values are |
57 | * 'y' or 'n' |
58 | * |
59 | * @var string |
60 | */ |
61 | protected $includeFacets; |
62 | |
63 | /** |
64 | * Array of filters to apply to the search |
65 | * |
66 | * @var array |
67 | */ |
68 | protected $facetFilters = []; |
69 | |
70 | /** |
71 | * Array mapping a facet field to the AND/OR operator to use with it |
72 | * |
73 | * @var array |
74 | */ |
75 | protected $facetOperators = []; |
76 | |
77 | /** |
78 | * Sort option to apply |
79 | * |
80 | * @var string |
81 | */ |
82 | protected $sort; |
83 | |
84 | /** |
85 | * Options to limit the results by |
86 | * |
87 | * @var array |
88 | */ |
89 | protected $limiters = []; |
90 | |
91 | /** |
92 | * Mode to be effective in the search |
93 | * |
94 | * @var string |
95 | */ |
96 | protected $searchMode; |
97 | |
98 | /** |
99 | * Expanders to use. Comma separated. |
100 | * |
101 | * @var array |
102 | */ |
103 | protected $expanders = []; |
104 | |
105 | /** |
106 | * Requested level of detail to return the results with |
107 | * |
108 | * @var string |
109 | */ |
110 | protected $view; |
111 | |
112 | /** |
113 | * Number of records to return |
114 | * |
115 | * @var int |
116 | */ |
117 | protected $resultsPerPage; |
118 | |
119 | /** |
120 | * Page number of records to return. This is used in conjunction with the |
121 | * {@link $resultsPerPage} to determine the set of records to return. |
122 | * |
123 | * @var int |
124 | */ |
125 | protected $pageNumber; |
126 | |
127 | /** |
128 | * Whether or not to highlight the search term in the results. |
129 | * |
130 | * @var bool |
131 | */ |
132 | protected $highlight; |
133 | |
134 | /** |
135 | * Collection of user actions to apply to current request |
136 | * |
137 | * @var array |
138 | */ |
139 | protected $actions = []; |
140 | |
141 | /** |
142 | * Constructor |
143 | * |
144 | * Sets up the EDS API Search Request model |
145 | * |
146 | * @param array $parameters parameters to populate request |
147 | */ |
148 | public function __construct($parameters = []) |
149 | { |
150 | $this->setParameters($parameters); |
151 | } |
152 | |
153 | /** |
154 | * Format a date limiter |
155 | * |
156 | * @param string $filter Filter value |
157 | * |
158 | * @return string |
159 | */ |
160 | protected function formatDateLimiter($filter) |
161 | { |
162 | // PublicationDate:[xxxx TO xxxx] |
163 | $dates = substr($filter, 17); |
164 | $dates = substr($dates, 0, strlen($dates) - 1); |
165 | $parts = explode(' TO ', $dates, 2); |
166 | $start = $end = null; |
167 | if (count($parts) == 2) { |
168 | $start = trim($parts[0]); |
169 | $end = trim($parts[1]); |
170 | } |
171 | if ('*' == $start || null == $start) { |
172 | $start = '0000'; |
173 | } |
174 | if ('*' == $end || null == $end) { |
175 | $end = date('Y') + 1; |
176 | } |
177 | return "DT1:$start-01/$end-12"; |
178 | } |
179 | |
180 | /** |
181 | * Set properties from parameters |
182 | * |
183 | * @param array $parameters Parameters to set |
184 | * |
185 | * @return void |
186 | */ |
187 | public function setParameters($parameters = []) |
188 | { |
189 | foreach ($parameters as $key => $values) { |
190 | switch ($key) { |
191 | case 'filters': |
192 | foreach ($values as $filter) { |
193 | if (str_starts_with($filter, 'LIMIT|')) { |
194 | $this->addLimiter(substr($filter, 6)); |
195 | } elseif (str_starts_with($filter, 'EXPAND:')) { |
196 | $this->addExpander(substr($filter, 7)); |
197 | } elseif (str_starts_with($filter, 'SEARCHMODE:')) { |
198 | $this->searchMode = substr($filter, 11, null); |
199 | } elseif (str_starts_with($filter, 'PublicationDate')) { |
200 | $this->addLimiter($this->formatDateLimiter($filter)); |
201 | } else { |
202 | $this->addFilter($filter); |
203 | } |
204 | } |
205 | break; |
206 | default: |
207 | if (property_exists($this, $key)) { |
208 | $this->$key = $values; |
209 | } |
210 | } |
211 | } |
212 | } |
213 | |
214 | /** |
215 | * Converts properties to a querystring to send to the EdsAPI |
216 | * |
217 | * @return string |
218 | */ |
219 | public function convertToQueryString() |
220 | { |
221 | return http_build_query($this->convertToQueryStringParameterArray()); |
222 | } |
223 | |
224 | /** |
225 | * Converts properties to a querystring to send to the EdsAPI |
226 | * |
227 | * @return string |
228 | */ |
229 | public function convertToQueryStringParameterArray() |
230 | { |
231 | $qs = []; |
232 | if (isset($this->query) && 0 < count($this->query)) { |
233 | $formatQuery = function ($json) { |
234 | $query = json_decode($json, true); |
235 | $queryString = empty($query['bool']) |
236 | ? '' : ($query['bool'] . ','); |
237 | if (!empty($query['field'])) { |
238 | $queryString .= $query['field'] . ':'; |
239 | } |
240 | $queryString .= static::escapeSpecialCharacters($query['term']); |
241 | return $queryString; |
242 | }; |
243 | $qs['query-x'] = array_map($formatQuery, $this->query); |
244 | } |
245 | |
246 | if (isset($this->facetFilters) && 0 < count($this->facetFilters)) { |
247 | $filterId = 1; |
248 | $qs['facetfilter'] = []; |
249 | foreach ($this->facetFilters as $field => $values) { |
250 | $values = array_map(fn ($value) => static::escapeSpecialCharacters($value), $values); |
251 | $operator = $this->facetOperators[$field]; |
252 | if ('OR' == $operator) { |
253 | $valuesString = implode(',', array_map(fn ($value) => "{$field}:{$value}", $values)); |
254 | $qs['facetfilter'][] = "{$filterId},{$valuesString}"; |
255 | $filterId++; |
256 | } else { |
257 | foreach ($values as $value) { |
258 | $qs['facetfilter'][] = "{$filterId},{$field}:{$value}"; |
259 | $filterId++; |
260 | } |
261 | } |
262 | } |
263 | } |
264 | |
265 | if (isset($this->limiters) && 0 < count($this->limiters)) { |
266 | $qs['limiter'] = $this->limiters; |
267 | } |
268 | |
269 | if (isset($this->actions) && 0 < count($this->actions)) { |
270 | $qs['action-x'] = $this->actions; |
271 | } |
272 | |
273 | if (isset($this->includeFacets)) { |
274 | $qs['includefacets'] = $this->includeFacets; |
275 | } |
276 | |
277 | if (isset($this->sort)) { |
278 | $qs['sort'] = $this->sort; |
279 | } |
280 | |
281 | if (isset($this->searchMode)) { |
282 | $qs['searchmode'] = $this->searchMode; |
283 | } |
284 | |
285 | if (isset($this->expanders) && 0 < count($this->expanders)) { |
286 | $qs['expander'] = implode(',', $this->expanders); |
287 | } |
288 | |
289 | if (isset($this->view)) { |
290 | $qs['view'] = $this->view; |
291 | } |
292 | |
293 | if (isset($this->resultsPerPage)) { |
294 | $qs['resultsperpage'] = $this->resultsPerPage; |
295 | } |
296 | |
297 | if (isset($this->pageNumber)) { |
298 | $qs['pagenumber'] = $this->pageNumber; |
299 | } |
300 | |
301 | $highlightVal = isset($this->highlight) && $this->highlight ? 'y' : 'n'; |
302 | $qs['highlight'] = $highlightVal; |
303 | |
304 | return $qs; |
305 | } |
306 | |
307 | /** |
308 | * Converts properties to a search request JSON document to send to the EdsAPI |
309 | * |
310 | * @return string |
311 | */ |
312 | public function convertToSearchRequestJSON() |
313 | { |
314 | $json = new \stdClass(); |
315 | $json->SearchCriteria = new \stdClass(); |
316 | $json->RetrievalCriteria = new \stdClass(); |
317 | $json->Actions = null; |
318 | if (isset($this->query) && 0 < count($this->query)) { |
319 | $json->SearchCriteria->Queries = []; |
320 | foreach ($this->query as $queryJson) { |
321 | $query = json_decode($queryJson, true); |
322 | $queryObj = new \stdClass(); |
323 | if (!empty($query['bool'])) { |
324 | $queryObj->BooleanOperator = $query['bool']; |
325 | } |
326 | if (!empty($query['field'])) { |
327 | $queryObj->FieldCode = $query['field']; |
328 | } |
329 | $queryObj->Term = $query['term']; |
330 | $json->SearchCriteria->Queries[] = $queryObj; |
331 | } |
332 | } |
333 | |
334 | if (isset($this->facetFilters) && 0 < count($this->facetFilters)) { |
335 | $json->SearchCriteria->FacetFilters = []; |
336 | $id = 1; |
337 | foreach ($this->facetFilters as $field => $values) { |
338 | if ('OR' == $this->facetOperators[$field]) { |
339 | $filterObj = new \stdClass(); |
340 | $filterObj->FilterId = $id++; |
341 | $filterObj->FacetValues = []; |
342 | foreach ($values as $value) { |
343 | $valueObj = new \stdClass(); |
344 | $valueObj->Id = $field; |
345 | $valueObj->Value = $value; |
346 | $filterObj->FacetValues[] = $valueObj; |
347 | } |
348 | $json->SearchCriteria->FacetFilters[] = $filterObj; |
349 | } else { |
350 | foreach ($values as $value) { |
351 | $filterObj = new \stdClass(); |
352 | $filterObj->FilterId = $id++; |
353 | $valueObj = new \stdClass(); |
354 | $valueObj->Id = $field; |
355 | $valueObj->Value = $value; |
356 | $filterObj->FacetValues = [$valueObj]; |
357 | $json->SearchCriteria->FacetFilters[] = $filterObj; |
358 | } |
359 | } |
360 | } |
361 | } |
362 | |
363 | if (isset($this->limiters) && 0 < count($this->limiters)) { |
364 | $json->SearchCriteria->Limiters = []; |
365 | foreach ($this->limiters as $field => $values) { |
366 | // All EDS limiter values are combined as 'OR'. |
367 | // There is no alternate 'AND' syntax as with filters. |
368 | $limiterObj = new \stdClass(); |
369 | $limiterObj->Id = $field; |
370 | $limiterObj->Values = $values; |
371 | $json->SearchCriteria->Limiters[] = $limiterObj; |
372 | } |
373 | } |
374 | |
375 | if (isset($this->actions) && 0 < count($this->actions)) { |
376 | $json->Actions = $this->actions; |
377 | } |
378 | |
379 | $json->SearchCriteria->IncludeFacets = $this->includeFacets ?? 'y'; |
380 | |
381 | if (isset($this->sort)) { |
382 | $json->SearchCriteria->Sort = $this->sort; |
383 | } |
384 | |
385 | if (isset($this->searchMode)) { |
386 | $json->SearchCriteria->SearchMode = $this->searchMode; |
387 | } |
388 | |
389 | if (isset($this->expanders) && 0 < count($this->expanders)) { |
390 | $json->SearchCriteria->Expanders = $this->expanders; |
391 | } |
392 | |
393 | if (isset($this->view)) { |
394 | $json->RetrievalCriteria->View = $this->view; |
395 | } |
396 | |
397 | if (isset($this->resultsPerPage)) { |
398 | $json->RetrievalCriteria->ResultsPerPage = intval($this->resultsPerPage); |
399 | } |
400 | |
401 | if (isset($this->pageNumber)) { |
402 | $json->RetrievalCriteria->PageNumber = intval($this->pageNumber); |
403 | } |
404 | |
405 | $highlightVal = isset($this->highlight) && $this->highlight ? 'y' : 'n'; |
406 | $json->RetrievalCriteria->Highlight = $highlightVal; |
407 | |
408 | return json_encode($json, JSON_PRETTY_PRINT); |
409 | } |
410 | |
411 | /** |
412 | * Determines whether or not a querystring parameter is indexed |
413 | * |
414 | * @param string $value parameter key to check |
415 | * |
416 | * @return bool |
417 | */ |
418 | public static function isParameterIndexed($value) |
419 | { |
420 | // Indexed parameter names end with '-x' |
421 | return str_ends_with($value, '-x'); |
422 | } |
423 | |
424 | /** |
425 | * Get the querystring parameter name of an indexed parameter to send to the Eds |
426 | * Api |
427 | * |
428 | * @param string $value Indexed parameter name |
429 | * |
430 | * @return string |
431 | */ |
432 | public static function getIndexedParameterName($value) |
433 | { |
434 | // Indexed parameter names end with '-x' |
435 | return substr($value, 0, -2); |
436 | } |
437 | |
438 | /** |
439 | * Add a new action |
440 | * |
441 | * @param string $action Action to add to the existing collection of actions |
442 | * |
443 | * @return void |
444 | */ |
445 | public function addAction($action) |
446 | { |
447 | $this->actions[] = $action; |
448 | } |
449 | |
450 | /** |
451 | * Add a new query expression |
452 | * |
453 | * @param string $query Query expression to add |
454 | * |
455 | * @return void |
456 | */ |
457 | public function addQuery($query) |
458 | { |
459 | $this->query[] = $query; |
460 | } |
461 | |
462 | /** |
463 | * Add a new limiter |
464 | * |
465 | * @param string $limiter Limiter to add |
466 | * |
467 | * @return void |
468 | */ |
469 | public function addLimiter($limiter) |
470 | { |
471 | [$field, $value] = explode(':', $limiter); |
472 | if (!array_key_exists($field, $this->limiters)) { |
473 | $this->limiters[$field] = []; |
474 | } |
475 | $this->limiters[$field][] = $value; |
476 | } |
477 | |
478 | /** |
479 | * Add a new expander |
480 | * |
481 | * @param string $expander Expander to add |
482 | * |
483 | * @return void |
484 | */ |
485 | public function addExpander($expander) |
486 | { |
487 | $this->expanders[] = $expander; |
488 | } |
489 | |
490 | /** |
491 | * Add a new facet filter |
492 | * |
493 | * @param string $facetFilter Facet Filter to add |
494 | * |
495 | * @return void |
496 | */ |
497 | public function addfilter($facetFilter) |
498 | { |
499 | $filterComponents = explode(':', $facetFilter, 3); |
500 | if (count($filterComponents) < 3) { |
501 | [$field, $value] = $filterComponents; |
502 | // Default to AND, since it's already the default in EDS.ini. |
503 | $operator = 'AND'; |
504 | } else { |
505 | [$field, $operator, $value] = $filterComponents; |
506 | } |
507 | if (str_starts_with($field, '~')) { |
508 | $field = substr($field, 1); |
509 | $operator = 'OR'; |
510 | } |
511 | if (!array_key_exists($field, $this->facetFilters)) { |
512 | $this->facetFilters[$field] = []; |
513 | } |
514 | $this->facetFilters[$field][] = $value; |
515 | $this->facetOperators[$field] = $operator; |
516 | } |
517 | |
518 | /** |
519 | * Escape characters that may be present in the parameter syntax |
520 | * |
521 | * @param string $value The value to escape |
522 | * |
523 | * @return string The value with special characters escaped |
524 | */ |
525 | public static function escapeSpecialCharacters($value) |
526 | { |
527 | return addcslashes($value, ':,'); |
528 | } |
529 | |
530 | /** |
531 | * Escape characters that may be present in the action parameter syntax |
532 | * |
533 | * @param string $value The value to escape |
534 | * |
535 | * @return string The value with special characters escaped |
536 | */ |
537 | public static function escapeSpecialCharactersForActions($value) |
538 | { |
539 | return addcslashes($value, ':,()'); |
540 | } |
541 | |
542 | /** |
543 | * Magic getter |
544 | * |
545 | * @param string $property Property to retrieve |
546 | * |
547 | * @return mixed |
548 | */ |
549 | public function __get($property) |
550 | { |
551 | if (property_exists($this, $property)) { |
552 | return $this->$property; |
553 | } |
554 | } |
555 | |
556 | /** |
557 | * Magic setter |
558 | * |
559 | * @param string $property Property to set |
560 | * @param mixed $value Value to set |
561 | * |
562 | * @return SearchRequestModel |
563 | */ |
564 | public function __set($property, $value) |
565 | { |
566 | if (property_exists($this, $property)) { |
567 | $this->$property = $value; |
568 | } |
569 | |
570 | return $this; |
571 | } |
572 | } |