Completed
Push — master ( 1e45e0...9c8682 )
by
unknown
17s queued 14s
created

SolrSearch::searchSolr()   D

Complexity

Conditions 17
Paths 140

Size

Total Lines 105
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 17
eloc 64
c 2
b 0
f 0
nc 140
nop 2
dl 0
loc 105
rs 4.8833

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Kitodo\Dlf\Common;
4
5
use Kitodo\Dlf\Common\SolrSearchResult\ResultDocument;
6
use Kitodo\Dlf\Domain\Model\Collection;
7
use Kitodo\Dlf\Domain\Model\Document;
8
use Kitodo\Dlf\Domain\Repository\DocumentRepository;
9
use TYPO3\CMS\Core\Cache\CacheManager;
10
use TYPO3\CMS\Core\Utility\GeneralUtility;
11
use TYPO3\CMS\Core\Utility\MathUtility;
12
use TYPO3\CMS\Extbase\Persistence\Generic\QueryResult;
13
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
14
15
/**
16
 * Targeted towards being used in ``PaginateController`` (``<f:widget.paginate>``).
17
 *
18
 * Notes on implementation:
19
 * - `Countable`: `count()` returns the number of toplevel documents.
20
 * - `getNumLoadedDocuments()`: Number of toplevel documents that have been fetched from Solr.
21
 * - `ArrayAccess`/`Iterator`: Access *fetched* toplevel documents indexed in order of their ranking.
22
 */
23
class SolrSearch implements \Countable, \Iterator, \ArrayAccess, QueryResultInterface
24
{
25
    protected $result;
26
    protected $position = 0;
27
28
    /**
29
     *
30
     * @param DocumentRepository $documentRepository
31
     * @param QueryResult|Collection $collection
32
     * @param array $settings
33
     * @param array $searchParams
34
     * @param QueryResult $listedMetadata
35
     */
36
    public function __construct($documentRepository, $collection, $settings, $searchParams, $listedMetadata = null)
37
    {
38
        $this->documentRepository = $documentRepository;
0 ignored issues
show
Bug Best Practice introduced by
The property documentRepository does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
39
        $this->collection = $collection;
0 ignored issues
show
Bug Best Practice introduced by
The property collection does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
40
        $this->settings = $settings;
0 ignored issues
show
Bug Best Practice introduced by
The property settings does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
41
        $this->searchParams = $searchParams;
0 ignored issues
show
Bug Best Practice introduced by
The property searchParams does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
42
        $this->listedMetadata = $listedMetadata;
0 ignored issues
show
Bug Best Practice introduced by
The property listedMetadata does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
43
    }
44
45
    public function getNumLoadedDocuments()
46
    {
47
        return count($this->result['documents']);
48
    }
49
50
    public function count()
51
    {
52
        if ($this->result === null) {
53
            return 0;
54
        }
55
56
        return $this->result['numberOfToplevels'];
57
    }
58
59
    public function current()
60
    {
61
        return $this[$this->position];
62
    }
63
64
    public function key()
65
    {
66
        return $this->position;
67
    }
68
69
    public function next()
70
    {
71
        $this->position++;
72
    }
73
74
    public function rewind()
75
    {
76
        $this->position = 0;
77
    }
78
79
    public function valid()
80
    {
81
        return isset($this[$this->position]);
82
    }
83
84
    public function offsetExists($offset)
85
    {
86
        $idx = $this->result['document_keys'][$offset];
87
        return isset($this->result['documents'][$idx]);
88
    }
89
90
    public function offsetGet($offset)
91
    {
92
        $idx = $this->result['document_keys'][$offset];
93
        $document = $this->result['documents'][$idx] ?? null;
94
95
        if ($document !== null) {
96
            // It may happen that a Solr group only includes non-toplevel results,
97
            // in which case metadata of toplevel entry isn't yet filled.
98
            if (empty($document['metadata'])) {
99
                $document['metadata'] = $this->fetchToplevelMetadataFromSolr([
0 ignored issues
show
Bug introduced by
array('query' => 'uid:' ...ray('score' => 'desc')) of type array<string,array<string,string>|integer|string> is incompatible with the type integer expected by parameter $queryParams of Kitodo\Dlf\Common\SolrSe...levelMetadataFromSolr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

99
                $document['metadata'] = $this->fetchToplevelMetadataFromSolr(/** @scrutinizer ignore-type */ [
Loading history...
100
                    'query' => 'uid:' . $document['uid'],
101
                    'start' => 0,
102
                    'rows' => 1,
103
                    'sort' => ['score' => 'desc'],
104
                ])[$document['uid']] ?? [];
105
            }
106
107
            // get title of parent/grandparent/... if empty
108
            if (empty($document['title']) && $document['partOf'] > 0) {
109
                $superiorTitle = Doc::getTitle($document['partOf'], true);
110
                if (!empty($superiorTitle)) {
111
                    $document['title'] = '[' . $superiorTitle . ']';
112
                }
113
            }
114
        }
115
116
        return $document;
117
    }
118
119
    public function offsetSet($offset, $value)
120
    {
121
        throw new \Exception("SolrSearch: Modifying result list is not supported");
122
    }
123
124
    public function offsetUnset($offset)
125
    {
126
        throw new \Exception("SolrSearch: Modifying result list is not supported");
127
    }
128
129
    public function getSolrResults()
130
    {
131
        return $this->result['solrResults'];
132
    }
133
134
    public function getByUid($uid)
135
    {
136
        return $this->result['documents'][$uid];
137
    }
138
139
    public function getQuery()
140
    {
141
        return new SolrSearchQuery($this);
142
    }
143
144
    public function getFirst()
145
    {
146
        return $this[0];
147
    }
148
149
    public function toArray()
150
    {
151
        return array_values($this->result['documents']);
152
    }
153
154
    /**
155
     * Get total number of hits.
156
     *
157
     * This can be accessed in Fluid template using `.numFound`.
158
     */
159
    public function getNumFound()
160
    {
161
        return $this->result['numFound'];
162
    }
163
164
    public function prepare()
165
    {
166
        // Prepare query parameters.
167
        $params = [];
168
        $matches = [];
169
        $fields = Solr::getFields();
170
171
        // Set search query.
172
        if (
173
            (!empty($this->searchParams['fulltext']))
174
            || preg_match('/' . $fields['fulltext'] . ':\((.*)\)/', trim($this->searchParams['query']), $matches)
175
        ) {
176
            // If the query already is a fulltext query e.g using the facets
177
            $this->searchParams['query'] = empty($matches[1]) ? $this->searchParams['query'] : $matches[1];
0 ignored issues
show
Bug Best Practice introduced by
The property searchParams does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
178
            // Search in fulltext field if applicable. Query must not be empty!
179
            if (!empty($this->searchParams['query'])) {
180
                $query = $fields['fulltext'] . ':(' . Solr::escapeQuery(trim($this->searchParams['query'])) . ')';
181
            }
182
            $params['fulltext'] = true;
183
        } else {
184
            // Retain given search field if valid.
185
            if (!empty($this->searchParams['query'])) {
186
                $query = Solr::escapeQueryKeepField(trim($this->searchParams['query']), $this->settings['storagePid']);
187
            }
188
        }
189
190
        // Add extended search query.
191
        if (
192
            !empty($this->searchParams['extQuery'])
193
            && is_array($this->searchParams['extQuery'])
194
        ) {
195
            $allowedOperators = ['AND', 'OR', 'NOT'];
196
            $numberOfExtQueries = count($this->searchParams['extQuery']);
197
            for ($i = 0; $i < $numberOfExtQueries; $i++) {
198
                if (!empty($this->searchParams['extQuery'][$i])) {
199
                    if (
200
                        in_array($this->searchParams['extOperator'][$i], $allowedOperators)
201
                    ) {
202
                        if (!empty($query)) {
203
                            $query .= ' ' . $this->searchParams['extOperator'][$i] . ' ';
204
                        }
205
                        $query .= Indexer::getIndexFieldName($this->searchParams['extField'][$i], $this->settings['storagePid']) . ':(' . Solr::escapeQuery($this->searchParams['extQuery'][$i]) . ')';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $query does not seem to be defined for all execution paths leading up to this point.
Loading history...
206
                    }
207
                }
208
            }
209
        }
210
211
        // Add filter query for faceting.
212
        if (isset($this->searchParams['fq']) && is_array($this->searchParams['fq'])) {
213
            foreach ($this->searchParams['fq'] as $filterQuery) {
214
                $params['filterquery'][]['query'] = $filterQuery;
215
            }
216
        }
217
218
        // Add filter query for in-document searching.
219
        if (
220
            !empty($this->searchParams['documentId'])
221
            && MathUtility::canBeInterpretedAsInteger($this->searchParams['documentId'])
222
        ) {
223
            // Search in document and all subordinates (valid for up to three levels of hierarchy).
224
            $params['filterquery'][]['query'] = '_query_:"{!join from='
225
                . $fields['uid'] . ' to=' . $fields['partof'] . '}'
226
                . $fields['uid'] . ':{!join from=' . $fields['uid'] . ' to=' . $fields['partof'] . '}'
227
                . $fields['uid'] . ':' . $this->searchParams['documentId'] . '"' . ' OR {!join from='
228
                . $fields['uid'] . ' to=' . $fields['partof'] . '}'
229
                . $fields['uid'] . ':' . $this->searchParams['documentId'] . ' OR '
230
                . $fields['uid'] . ':' . $this->searchParams['documentId'];
231
        }
232
233
        // if a collection is given, we prepare the collection query string
234
        if ($this->collection) {
235
            if ($this->collection instanceof Collection) {
236
                $collectionsQueryString = '"' . $this->collection->getIndexName() . '"';
237
            } else {
238
                $collectionsQueryString = '';
239
                foreach ($this->collection as $index => $collectionEntry) {
240
                    $collectionsQueryString .= ($index > 0 ? ' OR ' : '') . '"' . $collectionEntry->getIndexName() . '"';
241
                }
242
            }
243
244
            if (empty($query)) {
245
                $params['filterquery'][]['query'] = 'toplevel:true';
246
                $params['filterquery'][]['query'] = 'partof:0';
247
            }
248
            $params['filterquery'][]['query'] = 'collection_faceting:(' . $collectionsQueryString . ')';
249
        }
250
251
        // Set some query parameters.
252
        $params['query'] = !empty($query) ? $query : '*';
253
254
        // order the results as given or by title as default
255
        if (!empty($this->searchParams['orderBy'])) {
256
            $querySort = [
257
                $this->searchParams['orderBy'] => $this->searchParams['order'],
258
            ];
259
        } else {
260
            $querySort = [
261
                'year_sorting' => 'asc',
262
                'title_sorting' => 'asc',
263
            ];
264
        }
265
266
        $params['sort'] = $querySort;
267
        $params['listMetadataRecords'] = [];
268
269
        // Restrict the fields to the required ones.
270
        $params['fields'] = 'uid,id,page,title,thumbnail,partof,toplevel,type';
271
272
        if ($this->listedMetadata) {
273
            foreach ($this->listedMetadata as $metadata) {
274
                if ($metadata->getIndexStored() || $metadata->getIndexIndexed()) {
275
                    $listMetadataRecord = $metadata->getIndexName() . '_' . ($metadata->getIndexTokenized() ? 't' : 'u') . ($metadata->getIndexStored() ? 's' : 'u') . ($metadata->getIndexIndexed() ? 'i' : 'u');
276
                    $params['fields'] .= ',' . $listMetadataRecord;
277
                    $params['listMetadataRecords'][$metadata->getIndexName()] = $listMetadataRecord;
278
                }
279
            }
280
        }
281
282
        $this->params = $params;
0 ignored issues
show
Bug Best Practice introduced by
The property params does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
283
284
        // Send off query to get total number of search results in advance
285
        $this->submit(0, 1, false);
286
    }
287
288
    public function submit($start, $rows, $processResults = true)
289
    {
290
        $params = $this->params;
291
        $params['start'] = $start;
292
        $params['rows'] = $rows;
293
294
        // Perform search.
295
        $result = $this->searchSolr($params, true);
296
297
        // Initialize values
298
        $documents = [];
299
300
        if ($processResults && $result['numFound'] > 0) {
301
            // flat array with uids from Solr search
302
            $documentSet = array_unique(array_column($result['documents'], 'uid'));
303
304
            if (empty($documentSet)) {
305
                // return nothing found
306
                $this->result = ['solrResults' => [], 'documents' => [], 'document_keys' => [], 'numFound' => 0];
307
                return;
308
            }
309
310
            // get the Extbase document objects for all uids
311
            $allDocuments = $this->documentRepository->findAllByUids($documentSet);
312
            $childrenOf = $this->documentRepository->findChildrenOfEach($documentSet);
313
314
            foreach ($result['documents'] as $doc) {
315
                if (empty($documents[$doc['uid']]) && $allDocuments[$doc['uid']]) {
316
                    $documents[$doc['uid']] = $allDocuments[$doc['uid']];
317
                }
318
                if ($documents[$doc['uid']]) {
319
                    if ($doc['toplevel'] === false) {
320
                        // this maybe a chapter, article, ..., year
321
                        if ($doc['type'] === 'year') {
322
                            continue;
323
                        }
324
                        if (!empty($doc['page'])) {
325
                            // it's probably a fulltext or metadata search
326
                            $searchResult = [];
327
                            $searchResult['page'] = $doc['page'];
328
                            $searchResult['thumbnail'] = $doc['thumbnail'];
329
                            $searchResult['structure'] = $doc['type'];
330
                            $searchResult['title'] = $doc['title'];
331
                            foreach ($params['listMetadataRecords'] as $indexName => $solrField) {
332
                                if (isset($doc['metadata'][$indexName])) {
333
                                    $searchResult['metadata'][$indexName] = $doc['metadata'][$indexName];
334
                                }
335
                            }
336
                            if ($this->searchParams['fulltext'] == '1') {
337
                                $searchResult['snippet'] = $doc['snippet'];
338
                                $searchResult['highlight'] = $doc['highlight'];
339
                                $searchResult['highlight_word'] = $this->searchParams['query'];
340
                            }
341
                            $documents[$doc['uid']]['searchResults'][] = $searchResult;
342
                        }
343
                    } else if ($doc['toplevel'] === true) {
344
                        foreach ($params['listMetadataRecords'] as $indexName => $solrField) {
345
                            if (isset($doc['metadata'][$indexName])) {
346
                                $documents[$doc['uid']]['metadata'][$indexName] = $doc['metadata'][$indexName];
347
                            }
348
                        }
349
                        if ($this->searchParams['fulltext'] != '1') {
350
                            $documents[$doc['uid']]['page'] = 1;
351
                            $children = $childrenOf[$doc['uid']] ?? [];
352
                            if (!empty($children)) {
353
                                $metadataOf = $this->fetchToplevelMetadataFromSolr([
0 ignored issues
show
Bug introduced by
array('query' => 'partof...t' => 0, 'rows' => 100) of type array<string,integer|string> is incompatible with the type integer expected by parameter $queryParams of Kitodo\Dlf\Common\SolrSe...levelMetadataFromSolr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

353
                                $metadataOf = $this->fetchToplevelMetadataFromSolr(/** @scrutinizer ignore-type */ [
Loading history...
354
                                    'query' => 'partof:' . $doc['uid'],
355
                                    'start' => 0,
356
                                    'rows' => 100,
357
                                ]);
358
                                foreach ($children as $docChild) {
359
                                    // We need only a few fields from the children, but we need them as array.
360
                                    $childDocument = [
361
                                        'thumbnail' => $docChild['thumbnail'],
362
                                        'title' => $docChild['title'],
363
                                        'structure' => $docChild['structure'],
364
                                        'metsOrderlabel' => $docChild['metsOrderlabel'],
365
                                        'uid' => $docChild['uid'],
366
                                        'metadata' => $metadataOf[$docChild['uid']],
367
                                    ];
368
                                    $documents[$doc['uid']]['children'][$docChild['uid']] = $childDocument;
369
                                }
370
                            }
371
                        }
372
                    }
373
                }
374
            }
375
        }
376
377
        $this->result = ['solrResults' => $result, 'numberOfToplevels' => $result['numberOfToplevels'], 'documents' => $documents, 'document_keys' => array_keys($documents), 'numFound' => $result['numFound']];
378
    }
379
380
    /**
381
     * Find all listed metadata using specified query params.
382
     *
383
     * @param int $queryParams
384
     * @return array
385
     */
386
    protected function fetchToplevelMetadataFromSolr($queryParams)
387
    {
388
        // Prepare query parameters.
389
        $params = $queryParams;
390
        $metadataArray = [];
391
392
        // Set some query parameters.
393
        $params['listMetadataRecords'] = [];
394
395
        // Restrict the fields to the required ones.
396
        $params['fields'] = 'uid,toplevel';
397
398
        if ($this->listedMetadata) {
399
            foreach ($this->listedMetadata as $metadata) {
400
                if ($metadata->getIndexStored() || $metadata->getIndexIndexed()) {
401
                    $listMetadataRecord = $metadata->getIndexName() . '_' . ($metadata->getIndexTokenized() ? 't' : 'u') . ($metadata->getIndexStored() ? 's' : 'u') . ($metadata->getIndexIndexed() ? 'i' : 'u');
402
                    $params['fields'] .= ',' . $listMetadataRecord;
403
                    $params['listMetadataRecords'][$metadata->getIndexName()] = $listMetadataRecord;
404
                }
405
            }
406
        }
407
        // Set filter query to just get toplevel documents.
408
        $params['filterquery'][] = ['query' => 'toplevel:true'];
409
410
        // Perform search.
411
        $result = $this->searchSolr($params, true);
412
413
        foreach ($result['documents'] as $doc) {
414
            $metadataArray[$doc['uid']] = $doc['metadata'];
415
        }
416
417
        return $metadataArray;
418
    }
419
420
    /**
421
     * Processes a search request
422
     *
423
     * @access public
424
     *
425
     * @param array $parameters: Additional search parameters
426
     * @param boolean $enableCache: Enable caching of Solr requests
427
     *
428
     * @return array The Apache Solr Documents that were fetched
429
     */
430
    protected function searchSolr($parameters = [], $enableCache = true)
431
    {
432
        // Set query.
433
        $parameters['query'] = isset($parameters['query']) ? $parameters['query'] : '*';
434
        $parameters['filterquery'] = isset($parameters['filterquery']) ? $parameters['filterquery'] : [];
435
436
        // Perform Solr query.
437
        // Instantiate search object.
438
        $solr = Solr::getInstance($this->settings['solrcore']);
439
        if (!$solr->ready) {
440
            Helper::log('Apache Solr not available', LOG_SEVERITY_ERROR);
441
            return [
442
                'documents' => [],
443
                'numberOfToplevels' => 0,
444
                'numFound' => 0,
445
            ];
446
        }
447
448
        $cacheIdentifier = '';
449
        $cache = null;
450
        // Calculate cache identifier.
451
        if ($enableCache === true) {
452
            $cacheIdentifier = Helper::digest($solr->core . print_r($parameters, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($parameters, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

452
            $cacheIdentifier = Helper::digest($solr->core . /** @scrutinizer ignore-type */ print_r($parameters, true));
Loading history...
453
            $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('tx_dlf_solr');
454
        }
455
        $resultSet = [
456
            'documents' => [],
457
            'numberOfToplevels' => 0,
458
            'numFound' => 0,
459
        ];
460
        if ($enableCache === false || ($entry = $cache->get($cacheIdentifier)) === false) {
461
            $selectQuery = $solr->service->createSelect($parameters);
462
463
            $grouping = $selectQuery->getGrouping();
464
            $grouping->addField('uid');
465
            $grouping->setLimit(100); // Results in group (TODO: check)
466
            $grouping->setNumberOfGroups(true);
467
468
            if ($parameters['fulltext'] === true) {
469
                // get highlighting component and apply settings
470
                $selectQuery->getHighlighting();
471
            }
472
473
            $solrRequest = $solr->service->createRequest($selectQuery);
474
475
            if ($parameters['fulltext'] === true) {
476
                // If it is a fulltext search, enable highlighting.
477
                // field for which highlighting is going to be performed,
478
                // is required if you want to have OCR highlighting
479
                $solrRequest->addParam('hl.ocr.fl', 'fulltext');
480
                // return the coordinates of highlighted search as absolute coordinates
481
                $solrRequest->addParam('hl.ocr.absoluteHighlights', 'on');
482
                // max amount of snippets for a single page
483
                $solrRequest->addParam('hl.snippets', 20);
484
                // we store the fulltext on page level and can disable this option
485
                $solrRequest->addParam('hl.ocr.trackPages', 'off');
486
            }
487
488
            // Perform search for all documents with the same uid that either fit to the search or marked as toplevel.
489
            $response = $solr->service->executeRequest($solrRequest);
490
            $result = $solr->service->createResult($selectQuery, $response);
491
492
            $uidGroup = $result->getGrouping()->getGroup('uid');
0 ignored issues
show
Bug introduced by
The method getGrouping() does not exist on Solarium\Core\Query\Result\ResultInterface. It seems like you code against a sub-type of Solarium\Core\Query\Result\ResultInterface such as Solarium\QueryType\Select\Result\Result. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

492
            $uidGroup = $result->/** @scrutinizer ignore-call */ getGrouping()->getGroup('uid');
Loading history...
493
            $resultSet['numberOfToplevels'] = $uidGroup->getNumberOfGroups();
494
            $resultSet['numFound'] = $uidGroup->getMatches();
495
            $highlighting = [];
496
            if ($parameters['fulltext'] === true) {
497
                $data = $result->getData();
498
                $highlighting = $data['ocrHighlighting'];
499
            }
500
            $fields = Solr::getFields();
501
502
            foreach ($uidGroup as $group) {
503
                foreach ($group as $record) {
504
                    $resultDocument = new ResultDocument($record, $highlighting, $fields);
505
506
                    $document = [
507
                        'id' => $resultDocument->getId(),
508
                        'page' => $resultDocument->getPage(),
509
                        'snippet' => $resultDocument->getSnippets(),
510
                        'thumbnail' => $resultDocument->getThumbnail(),
511
                        'title' => $resultDocument->getTitle(),
512
                        'toplevel' => $resultDocument->getToplevel(),
513
                        'type' => $resultDocument->getType(),
514
                        'uid' => !empty($resultDocument->getUid()) ? $resultDocument->getUid() : $parameters['uid'],
515
                        'highlight' => $resultDocument->getHighlightsIds(),
516
                    ];
517
                    foreach ($parameters['listMetadataRecords'] as $indexName => $solrField) {
518
                        if (!empty($record->$solrField)) {
519
                            $document['metadata'][$indexName] = $record->$solrField;
520
                        }
521
                    }
522
                    $resultSet['documents'][] = $document;
523
                }
524
            }
525
526
            // Save value in cache.
527
            if (!empty($resultSet) && $enableCache === true) {
528
                $cache->set($cacheIdentifier, $resultSet);
529
            }
530
        } else {
531
            // Return cache hit.
532
            $resultSet = $entry;
533
        }
534
        return $resultSet;
535
    }
536
}
537