Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.87% |
111 / 117 |
|
20.00% |
1 / 5 |
CRAP | |
0.00% |
0 / 1 |
QueryAdapter | |
94.87% |
111 / 117 |
|
20.00% |
1 / 5 |
39.21 | |
0.00% |
0 / 1 |
deminify | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
6 | |||
display | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
displayAdvanced | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
7.02 | |||
fromRequest | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
13.08 | |||
minify | |
97.30% |
36 / 37 |
|
0.00% |
0 / 1 |
10 |
1 | <?php |
2 | |
3 | /** |
4 | * Search query adapter |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2011. |
9 | * Copyright (C) The National Library of Finland 2024. |
10 | * |
11 | * This program is free software; you can redistribute it and/or modify |
12 | * it under the terms of the GNU General Public License version 2, |
13 | * as published by the Free Software Foundation. |
14 | * |
15 | * This program is distributed in the hope that it will be useful, |
16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
18 | * GNU General Public License for more details. |
19 | * |
20 | * You should have received a copy of the GNU General Public License |
21 | * along with this program; if not, write to the Free Software |
22 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 | * |
24 | * @category VuFind |
25 | * @package Search_Solr |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
28 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
29 | * @link https://vufind.org Main Page |
30 | */ |
31 | |
32 | namespace VuFind\Search; |
33 | |
34 | use Laminas\Stdlib\Parameters; |
35 | use VuFindSearch\Query\AbstractQuery; |
36 | use VuFindSearch\Query\Query; |
37 | use VuFindSearch\Query\QueryGroup; |
38 | use VuFindSearch\Query\WorkKeysQuery; |
39 | |
40 | use function array_key_exists; |
41 | use function call_user_func; |
42 | use function count; |
43 | |
44 | /** |
45 | * Search query adapter |
46 | * |
47 | * @category VuFind |
48 | * @package Search_Solr |
49 | * @author Demian Katz <demian.katz@villanova.edu> |
50 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
51 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
52 | * @link https://vufind.org Main Page |
53 | */ |
54 | class QueryAdapter implements QueryAdapterInterface |
55 | { |
56 | /** |
57 | * Return a Query or QueryGroup based on minified search arguments. |
58 | * |
59 | * @param array $search Minified search arguments |
60 | * |
61 | * @return Query|QueryGroup|WorkKeysQuery |
62 | */ |
63 | public function deminify(array $search) |
64 | { |
65 | $type = $search['s'] ?? null; |
66 | if ('w' === $type) { |
67 | // WorkKeysQuery |
68 | return new WorkKeysQuery($search['l'], $search['i'], $search['k'] ?? []); |
69 | } |
70 | // Use array_key_exists since null is also valid |
71 | if ('b' === $type || array_key_exists('l', $search)) { |
72 | // Basic search |
73 | $handler = $search['i'] ?? $search['f']; |
74 | return new Query( |
75 | $search['l'], |
76 | $handler, |
77 | $search['o'] ?? null |
78 | ); |
79 | } elseif (isset($search['g'])) { |
80 | $operator = $search['g'][0]['b']; |
81 | return new QueryGroup( |
82 | $operator, |
83 | array_map([$this, 'deminify'], $search['g']) |
84 | ); |
85 | } else { |
86 | // Special case: The outer-most group-of-groups. |
87 | if (isset($search[0]['j'])) { |
88 | $operator = $search[0]['j']; |
89 | return new QueryGroup( |
90 | $operator, |
91 | array_map([$this, 'deminify'], $search) |
92 | ); |
93 | } else { |
94 | // Simple query |
95 | return new Query($search[0]['l'], $search[0]['i']); |
96 | } |
97 | } |
98 | } |
99 | |
100 | /** |
101 | * Convert a Query or QueryGroup into a human-readable display query. |
102 | * |
103 | * @param AbstractQuery $query Query to convert |
104 | * @param callable $translate Callback to translate strings |
105 | * @param callable $showName Callback to translate field names |
106 | * |
107 | * @return string |
108 | */ |
109 | public function display(AbstractQuery $query, $translate, $showName) |
110 | { |
111 | // Simple case -- basic query: |
112 | if ($query instanceof Query) { |
113 | return $query->getString(); |
114 | } |
115 | |
116 | // Work keys query: |
117 | if ($query instanceof WorkKeysQuery) { |
118 | return $translate('Versions') . ' - ' . ($query->getId() ?? ''); |
119 | } |
120 | |
121 | // Complex case -- advanced query: |
122 | return $this->displayAdvanced($query, $translate, $showName); |
123 | } |
124 | |
125 | /** |
126 | * Support method for display() -- process advanced queries. |
127 | * |
128 | * @param QueryGroup $query Query to convert |
129 | * @param callable $translate Callback to translate strings |
130 | * @param callable $showName Callback to translate field names |
131 | * |
132 | * @return string |
133 | */ |
134 | protected function displayAdvanced( |
135 | QueryGroup $query, |
136 | callable $translate, |
137 | callable $showName |
138 | ) { |
139 | // Groups and exclusions. |
140 | $groups = $excludes = []; |
141 | |
142 | foreach ($query->getQueries() as $search) { |
143 | if ($search instanceof QueryGroup) { |
144 | $thisGroup = []; |
145 | // Process each search group |
146 | foreach ($search->getQueries() as $group) { |
147 | if ($group instanceof Query) { |
148 | // Build this group individually as a basic search |
149 | $thisGroup[] |
150 | = call_user_func($showName, $group->getHandler()) . ':' |
151 | . $group->getString(); |
152 | } else { |
153 | throw new \Exception('Unexpected ' . $group::class); |
154 | } |
155 | } |
156 | // Is this an exclusion (NOT) group or a normal group? |
157 | $str = implode( |
158 | ' ' . call_user_func($translate, $search->getOperator()) |
159 | . ' ', |
160 | $thisGroup |
161 | ); |
162 | if ($search->isNegated()) { |
163 | $excludes[] = $str; |
164 | } else { |
165 | $groups[] = $str; |
166 | } |
167 | } else { |
168 | throw new \Exception('Unexpected ' . $search::class); |
169 | } |
170 | } |
171 | |
172 | // Base 'advanced' query |
173 | $operator = call_user_func($translate, $query->getOperator()); |
174 | $output = '(' . implode(') ' . $operator . ' (', $groups) . ')'; |
175 | |
176 | // Concatenate exclusion after that |
177 | if (count($excludes) > 0) { |
178 | $output .= ' ' . call_user_func($translate, 'NOT') . ' ((' |
179 | . implode(') ' . call_user_func($translate, 'OR') . ' (', $excludes) |
180 | . '))'; |
181 | } |
182 | |
183 | return $output; |
184 | } |
185 | |
186 | /** |
187 | * Convert user request parameters into a query (currently for advanced searches |
188 | * and work keys searches only). |
189 | * |
190 | * @param Parameters $request User-submitted parameters |
191 | * @param string $defaultHandler Default search handler |
192 | * |
193 | * @return Query|QueryGroup|WorkKeysQuery |
194 | */ |
195 | public function fromRequest(Parameters $request, $defaultHandler) |
196 | { |
197 | // Check for a work keys query first (id and keys included for back-compatibility): |
198 | if ( |
199 | $request->get('search') === 'versions' |
200 | || ($request->offsetExists('id') && $request->offsetExists('keys')) |
201 | ) { |
202 | if (null !== ($id = $request->offsetGet('id'))) { |
203 | return new WorkKeysQuery($id, true); |
204 | } |
205 | } |
206 | |
207 | $groups = []; |
208 | // Loop through all parameters and look for 'lookforX' |
209 | foreach ($request as $key => $value) { |
210 | if (!preg_match('/^lookfor(\d+)$/', $key, $matches)) { |
211 | continue; |
212 | } |
213 | $groupId = $matches[1]; |
214 | $group = []; |
215 | $lastBool = null; |
216 | $value = (array)$value; |
217 | |
218 | // Loop through each term inside the group |
219 | for ($i = 0; $i < count($value); $i++) { |
220 | // Ignore advanced search fields with no lookup |
221 | if ($value[$i] != '') { |
222 | // Use default fields if not set |
223 | $typeArr = (array)$request->get("type$groupId"); |
224 | $handler = !empty($typeArr[$i]) ? $typeArr[$i] : $defaultHandler; |
225 | |
226 | $opArr = (array)$request->get("op$groupId"); |
227 | $operator = !empty($opArr[$i]) ? $opArr[$i] : null; |
228 | |
229 | // Add term to this group |
230 | $boolArr = (array)$request->get("bool$groupId"); |
231 | $lastBool = $boolArr[0] ?? 'AND'; |
232 | $group[] = new Query($value[$i], $handler, $operator); |
233 | } |
234 | } |
235 | |
236 | // Make sure we aren't adding groups that had no terms |
237 | if (count($group) > 0) { |
238 | // Add the completed group to the list |
239 | $groups[] = new QueryGroup($lastBool, $group); |
240 | } |
241 | } |
242 | |
243 | return (count($groups) > 0) |
244 | ? new QueryGroup($request->get('join', 'AND'), $groups) |
245 | : new Query(); |
246 | } |
247 | |
248 | /** |
249 | * Convert a Query or QueryGroup into minified search arguments. |
250 | * |
251 | * @param AbstractQuery $query Query to minify |
252 | * @param bool $topLevel Is this a top-level query? (Used for recursion) |
253 | * |
254 | * @return array |
255 | */ |
256 | public function minify(AbstractQuery $query, $topLevel = true) |
257 | { |
258 | // Simple query: |
259 | if ($query instanceof Query) { |
260 | return [ |
261 | [ |
262 | 'l' => $query->getString(), |
263 | 'i' => $query->getHandler(), |
264 | 's' => 'b', |
265 | ], |
266 | ]; |
267 | } |
268 | |
269 | // WorkKeys query: |
270 | if ($query instanceof WorkKeysQuery) { |
271 | return [ |
272 | 'l' => $query->getId(), |
273 | 'i' => $query->getIncludeSelf(), |
274 | 'k' => $query->getWorkKeys(), |
275 | 's' => 'w', |
276 | ]; |
277 | } |
278 | |
279 | // Advanced query: |
280 | $retVal = []; |
281 | $operator = $query->isNegated() ? 'NOT' : $query->getOperator(); |
282 | foreach ($query->getQueries() as $current) { |
283 | if ($topLevel) { |
284 | $retVal[] = [ |
285 | 'g' => $this->minify($current, false), |
286 | 'j' => $operator, |
287 | 's' => 'a', |
288 | ]; |
289 | } elseif ($current instanceof QueryGroup) { |
290 | throw new \Exception('Not sure how to minify this query!'); |
291 | } else { |
292 | $currentArr = [ |
293 | 'f' => $current->getHandler(), |
294 | 'l' => $current->getString(), |
295 | 'b' => $operator, |
296 | ]; |
297 | if (null !== ($op = $current->getOperator())) { |
298 | // Some search forms omit the operator for the first element; |
299 | // if we have an operator in a subsequent element, we should |
300 | // backfill a blank here for consistency; otherwise, VuFind |
301 | // may not construct correct search URLs. |
302 | if (isset($retVal[0]['f']) && !isset($retVal[0]['o'])) { |
303 | $retVal[0]['o'] = ''; |
304 | } |
305 | $currentArr['o'] = $op; |
306 | } |
307 | $retVal[] = $currentArr; |
308 | } |
309 | } |
310 | return $retVal; |
311 | } |
312 | } |