Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.05% covered (warning)
86.05%
37 / 43
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Backend
86.05% covered (warning)
86.05%
37 / 43
72.73% covered (warning)
72.73%
8 / 11
17.79
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setMaxQueryTime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSearchProgressTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 search
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
4
 retrieve
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryBuilder
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRecordCollectionFactory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getConnector
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createRecordCollection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSearchProgress
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Pazpar2 backend.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Search
25 * @author   David Maus <maus@hab.de>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org
28 */
29
30namespace VuFindSearch\Backend\Pazpar2;
31
32use VuFindSearch\Backend\AbstractBackend;
33use VuFindSearch\ParamBag;
34use VuFindSearch\Query\AbstractQuery;
35use VuFindSearch\Response\RecordCollectionFactoryInterface;
36use VuFindSearch\Response\RecordCollectionInterface;
37
38use function intval;
39
40/**
41 * Pazpar2 backend.
42 *
43 * @category VuFind
44 * @package  Search
45 * @author   David Maus <maus@hab.de>
46 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
47 * @link     https://vufind.org
48 */
49class Backend extends AbstractBackend
50{
51    /**
52     * Connector.
53     *
54     * @var Connector
55     */
56    protected $connector;
57
58    /**
59     * Query builder.
60     *
61     * @var QueryBuilder
62     */
63    protected $queryBuilder = null;
64
65    /**
66     * How much search progress should be completed before returning results
67     * (a value between 0 and 1).
68     *
69     * @var float
70     */
71    protected $progressTarget = 1.0;
72
73    /**
74     * The maximum amount of time to wait to reach $progressTarget (above)
75     * before giving up and accepting what is currently available. (Measured
76     * in seconds).
77     *
78     * @var int
79     */
80    protected $maxQueryTime = 60;
81
82    /**
83     * Constructor.
84     *
85     * @param Connector                        $connector Pazpar2 connector
86     * @param RecordCollectionFactoryInterface $factory   Record collection factory
87     *
88     * @return void
89     */
90    public function __construct(
91        Connector $connector,
92        RecordCollectionFactoryInterface $factory = null
93    ) {
94        if (null !== $factory) {
95            $this->setRecordCollectionFactory($factory);
96        }
97        $this->connector      = $connector;
98    }
99
100    /**
101     * Set the max query time.
102     *
103     * @param int $time New value
104     *
105     * @return void
106     */
107    public function setMaxQueryTime($time)
108    {
109        $this->maxQueryTime = $time;
110    }
111
112    /**
113     * Set the search progress target.
114     *
115     * @param float $progress New value
116     *
117     * @return void
118     */
119    public function setSearchProgressTarget($progress)
120    {
121        $this->progressTarget = $progress;
122    }
123
124    /**
125     * Perform a search and return record collection.
126     *
127     * @param AbstractQuery $query  Search query
128     * @param int           $offset Search offset
129     * @param int           $limit  Search limit
130     * @param ParamBag      $params Search backend parameters
131     *
132     * @return RecordCollectionInterface
133     */
134    public function search(
135        AbstractQuery $query,
136        $offset,
137        $limit,
138        ParamBag $params = null
139    ) {
140        $baseParams = $this->getQueryBuilder()->build($query);
141        if (null !== $params) {
142            $baseParams->mergeWith($params);
143        }
144        $this->connector->search($baseParams);
145
146        /* Pazpar2 does not return all results immediately. Rather, we need to
147         * occasionally check with the Pazpar2 server on the status of the
148         * search.
149         *
150         * This loop will continue to wait until the configured level of
151         * progress is reached or until the maximum query time has passed at
152         * which time the existing results will be returned.
153         */
154        $queryStart = time();
155        $progress = $this->getSearchProgress();
156        while (
157            $progress < $this->progressTarget
158            && (time() - $queryStart) < $this->maxQueryTime
159        ) {
160            sleep(1);
161            $progress = $this->getSearchProgress();
162        }
163
164        $showParams = new ParamBag(
165            ['block' => 1, 'num' => $limit, 'start' => $offset]
166        );
167        $response = $this->connector->show($showParams);
168
169        $hits = $response->hit ?? [];
170        $collection = $this->createRecordCollection(
171            $hits,
172            intval($response->merged),
173            $offset
174        );
175        $this->injectSourceIdentifier($collection);
176        return $collection;
177    }
178
179    /**
180     * Retrieve a single document.
181     *
182     * @param string   $id     Document identifier
183     * @param ParamBag $params Search backend parameters
184     *
185     * @return RecordCollectionInterface
186     */
187    public function retrieve($id, ParamBag $params = null)
188    {
189        $response   = $this->connector->record($id);
190        $collection = $this->createRecordCollection([$response], 1);
191        $this->injectSourceIdentifier($collection);
192        return $collection;
193    }
194
195    /**
196     * Set the query builder.
197     *
198     * @param QueryBuilder $queryBuilder Query builder
199     *
200     * @return void
201     */
202    public function setQueryBuilder(QueryBuilder $queryBuilder)
203    {
204        $this->queryBuilder = $queryBuilder;
205    }
206
207    /**
208     * Return query builder.
209     *
210     * Lazy loads an empty QueryBuilder if none was set.
211     *
212     * @return QueryBuilder
213     */
214    public function getQueryBuilder()
215    {
216        if (!$this->queryBuilder) {
217            $this->queryBuilder = new QueryBuilder();
218        }
219        return $this->queryBuilder;
220    }
221
222    /**
223     * Return the record collection factory.
224     *
225     * Lazy loads a generic collection factory.
226     *
227     * @return RecordCollectionFactoryInterface
228     */
229    public function getRecordCollectionFactory()
230    {
231        if (!$this->collectionFactory) {
232            $this->collectionFactory = new Response\RecordCollectionFactory();
233        }
234        return $this->collectionFactory;
235    }
236
237    /**
238     * Return the Summon connector.
239     *
240     * @return Connector
241     */
242    public function getConnector()
243    {
244        return $this->connector;
245    }
246
247    /// Internal API
248
249    /**
250     * Create record collection.
251     *
252     * @param array $records Records to process
253     * @param int   $total   Total result count
254     * @param int   $offset  Search offset
255     *
256     * @return RecordCollectionInterface
257     *
258     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
259     */
260    protected function createRecordCollection($records, $total = 0, $offset = 0)
261    {
262        return $this->getRecordCollectionFactory()
263            ->factory(compact('records', 'total', 'offset'));
264    }
265
266    /**
267     * Get progress on the current search operation.
268     *
269     * @return float
270     */
271    protected function getSearchProgress()
272    {
273        $statResponse = $this->connector->stat();
274        return (float)$statResponse->progress;
275    }
276}