1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace POData\UriProcessor\QueryProcessor; |
6
|
|
|
|
7
|
|
|
use POData\Common\InvalidOperationException; |
8
|
|
|
use POData\Common\Messages; |
9
|
|
|
use POData\Common\NotImplementedException; |
10
|
|
|
use POData\Common\ODataConstants; |
11
|
|
|
use POData\Common\ODataException; |
12
|
|
|
use POData\IService; |
13
|
|
|
use POData\Providers\Metadata\ResourceTypeKind; |
14
|
|
|
use POData\Providers\Metadata\Type\Int32; |
15
|
|
|
use POData\Providers\Query\QueryType; |
16
|
|
|
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandProjectionParser; |
17
|
|
|
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionParser2; |
18
|
|
|
use POData\UriProcessor\QueryProcessor\OrderByParser\OrderByParser; |
19
|
|
|
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenParser; |
20
|
|
|
use POData\UriProcessor\RequestDescription; |
21
|
|
|
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind; |
22
|
|
|
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetSource; |
23
|
|
|
use ReflectionException; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Class QueryProcessor. |
27
|
|
|
*/ |
28
|
|
|
class QueryProcessor |
29
|
|
|
{ |
30
|
|
|
/** |
31
|
|
|
* Holds details of the request that client has submitted. |
32
|
|
|
* |
33
|
|
|
* @var RequestDescription |
34
|
|
|
*/ |
35
|
|
|
private $request; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Holds reference to the underlying data service specific |
39
|
|
|
* instance. |
40
|
|
|
* |
41
|
|
|
* @var IService |
42
|
|
|
*/ |
43
|
|
|
private $service; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* If $orderby, $skip, $top and $count options can be applied to the request. |
47
|
|
|
* |
48
|
|
|
* @var bool |
49
|
|
|
*/ |
50
|
|
|
private $setQueryApplicable; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Whether the top level request is a candidate for paging. |
54
|
|
|
* |
55
|
|
|
* @var bool |
56
|
|
|
*/ |
57
|
|
|
private $pagingApplicable; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Whether $expand, $select can be applied to the request. |
61
|
|
|
* |
62
|
|
|
* @var bool |
63
|
|
|
*/ |
64
|
|
|
private $expandSelectApplicable; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Creates new instance of QueryProcessor. |
68
|
|
|
* |
69
|
|
|
* @param RequestDescription $request Description of the request submitted by client |
70
|
|
|
* @param IService $service Reference to the service implementation |
71
|
|
|
*/ |
72
|
|
|
private function __construct(RequestDescription $request, IService $service) |
73
|
|
|
{ |
74
|
|
|
$this->request = $request; |
75
|
|
|
$this->service = $service; |
76
|
|
|
|
77
|
|
|
$isSingleResult = $request->isSingleResult(); |
78
|
|
|
|
79
|
|
|
//$top, $skip, $order, $inlinecount & $count are only applicable if: |
80
|
|
|
//The query targets a resource collection |
81
|
|
|
$this->setQueryApplicable = ($request->getTargetKind() == TargetKind::RESOURCE() && !$isSingleResult); |
82
|
|
|
//Or it's a $count resource (although $inlinecount isn't applicable in this case.. |
83
|
|
|
//but there's a check somewhere else for this |
84
|
|
|
$this->setQueryApplicable |= $request->queryType == QueryType::COUNT(); |
85
|
|
|
|
86
|
|
|
//Paging is allowed if |
87
|
|
|
//The request targets a resource collection |
88
|
|
|
//and the request isn't for a $count segment |
89
|
|
|
$this->pagingApplicable = $this->request->getTargetKind() == TargetKind::RESOURCE() |
90
|
|
|
&& !$isSingleResult |
91
|
|
|
&& ($request->queryType != QueryType::COUNT()); |
92
|
|
|
|
93
|
|
|
$targetResourceType = $this->request->getTargetResourceType(); |
94
|
|
|
$targetResourceSetWrapper = $this->request->getTargetResourceSetWrapper(); |
95
|
|
|
|
96
|
|
|
$this->expandSelectApplicable = null !== $targetResourceType |
97
|
|
|
&& null !== $targetResourceSetWrapper |
98
|
|
|
&& $targetResourceType->getResourceTypeKind() == ResourceTypeKind::ENTITY() |
99
|
|
|
&& !$this->request->isLinkUri(); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Process the OData query options and update RequestDescription accordingly. |
104
|
|
|
* |
105
|
|
|
* @param RequestDescription $request Description of the request submitted by client |
106
|
|
|
* @param IService $service Reference to the data service |
107
|
|
|
* |
108
|
|
|
* @throws ODataException |
109
|
|
|
* @throws NotImplementedException |
110
|
|
|
* @throws InvalidOperationException |
111
|
|
|
* @throws ReflectionException |
112
|
|
|
*/ |
113
|
|
|
public static function process(RequestDescription $request, IService $service): void |
114
|
|
|
{ |
115
|
|
|
$queryProcessor = new self($request, $service); |
116
|
|
|
if ($request->getTargetSource() == TargetSource::NONE()) { |
117
|
|
|
//A service directory, metadata or batch request |
118
|
|
|
$queryProcessor->checkForEmptyQueryArguments(); |
119
|
|
|
} else { |
120
|
|
|
$queryProcessor->processQuery(); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
unset($queryProcessor); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Checks whether client request contains any odata query options. |
128
|
|
|
* |
129
|
|
|
* |
130
|
|
|
* @throws ODataException Throws bad request error if client request |
131
|
|
|
* includes any odata query option |
132
|
|
|
*/ |
133
|
|
|
private function checkForEmptyQueryArguments(): void |
134
|
|
|
{ |
135
|
|
|
$serviceHost = $this->service->getHost(); |
136
|
|
|
$items = [ |
137
|
|
|
ODataConstants::HTTPQUERY_STRING_FILTER, |
138
|
|
|
ODataConstants::HTTPQUERY_STRING_EXPAND, |
139
|
|
|
ODataConstants::HTTPQUERY_STRING_INLINECOUNT, |
140
|
|
|
ODataConstants::HTTPQUERY_STRING_ORDERBY, |
141
|
|
|
ODataConstants::HTTPQUERY_STRING_SELECT, |
142
|
|
|
ODataConstants::HTTPQUERY_STRING_SKIP, |
143
|
|
|
ODataConstants::HTTPQUERY_STRING_SKIPTOKEN, |
144
|
|
|
ODataConstants::HTTPQUERY_STRING_TOP |
145
|
|
|
]; |
146
|
|
|
|
147
|
|
|
$allNull = true; |
148
|
|
|
foreach ($items as $queryItem) { |
149
|
|
|
$item = $serviceHost->getQueryStringItem($queryItem); |
150
|
|
|
$currentNull = null === $item; |
151
|
|
|
$allNull = ($currentNull && $allNull); |
152
|
|
|
if (false === $allNull) { |
153
|
|
|
break; |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
if (false === $allNull) { |
158
|
|
|
throw ODataException::createBadRequestError( |
159
|
|
|
Messages::queryProcessorNoQueryOptionsApplicable() |
160
|
|
|
); |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Processes the odata query options in the request uri and update the request description |
166
|
|
|
* instance with processed details. |
167
|
|
|
* |
168
|
|
|
* @throws ODataException If any error occurred while processing the query options |
169
|
|
|
* @throws NotImplementedException |
170
|
|
|
* @throws InvalidOperationException |
171
|
|
|
* @throws ReflectionException |
172
|
|
|
*/ |
173
|
|
|
private function processQuery(): void |
174
|
|
|
{ |
175
|
|
|
$this->processSkipAndTop(); |
176
|
|
|
$this->processOrderBy(); |
177
|
|
|
$this->processFilter(); |
178
|
|
|
$this->processCount(); |
179
|
|
|
$this->processSkipToken(); |
180
|
|
|
$this->processExpandAndSelect(); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Process $skip and $top options. |
185
|
|
|
* |
186
|
|
|
* |
187
|
|
|
* @throws ODataException Throws syntax error if the $skip or $top option |
188
|
|
|
* is specified with non-integer value, throws |
189
|
|
|
* bad request error if the $skip or $top option |
190
|
|
|
* is not applicable for the requested resource |
191
|
|
|
*/ |
192
|
|
|
private function processSkipAndTop(): void |
193
|
|
|
{ |
194
|
|
|
$value = null; |
195
|
|
|
if ($this->readSkipOrTopOption(ODataConstants::HTTPQUERY_STRING_SKIP, $value)) { |
196
|
|
|
$this->request->setSkipCount($value); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
$pageSize = 0; |
200
|
|
|
$isPagingRequired = $this->isSSPagingRequired(); |
201
|
|
|
if ($isPagingRequired) { |
202
|
|
|
$pageSize = $this->request |
203
|
|
|
->getTargetResourceSetWrapper() |
204
|
|
|
->getResourceSetPageSize(); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
if ($this->readSkipOrTopOption(ODataConstants::HTTPQUERY_STRING_TOP, $value)) { |
208
|
|
|
$this->request->setTopOptionCount($value); |
209
|
|
|
if ($isPagingRequired && $pageSize < $value) { |
210
|
|
|
//If $top is greater than or equal to page size, |
211
|
|
|
//we will need a $skiptoken and thus our response |
212
|
|
|
//will be 2.0 |
213
|
|
|
$this->request->raiseResponseVersion(2, 0); |
214
|
|
|
$this->request->setTopCount($pageSize); |
215
|
|
|
} else { |
216
|
|
|
$this->request->setTopCount($value); |
217
|
|
|
} |
218
|
|
|
} elseif ($isPagingRequired) { |
219
|
|
|
$this->request->raiseResponseVersion(2, 0); |
220
|
|
|
$this->request->setTopCount($pageSize); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
if (null !== $this->request->getSkipCount() |
224
|
|
|
|| null !== $this->request->getTopCount() |
225
|
|
|
) { |
226
|
|
|
$this->checkSetQueryApplicable(); |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Read skip or top query option value which is expected to be positive |
232
|
|
|
* integer. |
233
|
|
|
* |
234
|
|
|
* @param string $queryItem The name of the query item to read from request |
235
|
|
|
* uri ($skip or $top) |
236
|
|
|
* @param int &$value On return, If the requested query item is |
237
|
|
|
* present with a valid integer value then this |
238
|
|
|
* argument will holds that integer value |
239
|
|
|
* otherwise holds zero |
240
|
|
|
* |
241
|
|
|
* @throws ODataException Throws syntax error if the requested argument |
242
|
|
|
* is present and it is not an integer |
243
|
|
|
* @return bool True If the requested query item with valid integer |
244
|
|
|
* value is present in the request, false query |
245
|
|
|
* item is absent in the request uri |
246
|
|
|
*/ |
247
|
|
|
private function readSkipOrTopOption(string $queryItem, ?int &$value): bool |
248
|
|
|
{ |
249
|
|
|
$value = $this->service->getHost()->getQueryStringItem($queryItem); |
250
|
|
|
if (null !== $value) { |
251
|
|
|
$int = new Int32(); |
252
|
|
|
if (!$int->validate($value, $outValue)) { |
253
|
|
|
throw ODataException::createSyntaxError( |
254
|
|
|
Messages::queryProcessorIncorrectArgumentFormat( |
255
|
|
|
$queryItem, |
256
|
|
|
$value |
257
|
|
|
) |
258
|
|
|
); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
$value = intval($value); |
262
|
|
|
if (0 > $value) { |
263
|
|
|
throw ODataException::createSyntaxError( |
264
|
|
|
Messages::queryProcessorIncorrectArgumentFormat( |
265
|
|
|
$queryItem, |
266
|
|
|
$value |
267
|
|
|
) |
268
|
|
|
); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
return true; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
$value = 0; |
275
|
|
|
|
276
|
|
|
return false; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Is server side paging is configured, this function return true |
281
|
|
|
* if the resource targeted by the resource path is applicable |
282
|
|
|
* for paging and paging is enabled for the targeted resource set |
283
|
|
|
* else false. |
284
|
|
|
* |
285
|
|
|
* @return bool |
286
|
|
|
*/ |
287
|
|
|
private function isSSPagingRequired(): bool |
288
|
|
|
{ |
289
|
|
|
if ($this->pagingApplicable) { |
290
|
|
|
$targetResourceSetWrapper = $this->request->getTargetResourceSetWrapper(); |
291
|
|
|
//assert($targetResourceSetWrapper != NULL) |
292
|
|
|
return 0 != $targetResourceSetWrapper->getResourceSetPageSize(); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
return false; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* To check whether the the query options $orderby, $inlinecount, $skip |
300
|
|
|
* or $top is applicable for the current requested resource. |
301
|
|
|
* |
302
|
|
|
* |
303
|
|
|
* @throws ODataException Throws bad request error if any of the query options $orderby, $inlinecount, |
304
|
|
|
* $skip or $top cannot be applied to the requested resource |
305
|
|
|
*/ |
306
|
|
|
private function checkSetQueryApplicable(): void |
307
|
|
|
{ |
308
|
|
|
if (!$this->setQueryApplicable) { |
309
|
|
|
throw ODataException::createBadRequestError( |
310
|
|
|
Messages::queryProcessorQuerySetOptionsNotApplicable() |
311
|
|
|
); |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Process $orderby option, This function requires _processSkipAndTopOption |
317
|
|
|
* function to be already called as this function need to know whether |
318
|
|
|
* client has requested for skip, top or paging is enabled for the |
319
|
|
|
* requested resource in these cases function generates additional orderby |
320
|
|
|
* expression using keys. |
321
|
|
|
* |
322
|
|
|
* |
323
|
|
|
* @throws ODataException If any error occurs while parsing orderby option |
324
|
|
|
* @throws InvalidOperationException |
325
|
|
|
* @throws ReflectionException |
326
|
|
|
*/ |
327
|
|
|
private function processOrderBy(): void |
328
|
|
|
{ |
329
|
|
|
$orderBy = $this->service->getHost()->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_ORDERBY); |
330
|
|
|
|
331
|
|
|
if (null !== $orderBy) { |
332
|
|
|
$this->checkSetQueryApplicable(); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
$targetResourceType = $this->request->getTargetResourceType(); |
336
|
|
|
assert($targetResourceType != null, 'Request target resource type must not be null'); |
337
|
|
|
/* |
338
|
|
|
* We need to do sorting in the folowing cases, irrespective of |
339
|
|
|
* $orderby clause is present or not. |
340
|
|
|
* 1. If $top or $skip is specified |
341
|
|
|
* skip and take will be applied on sorted list only. If $skip |
342
|
|
|
* is specified then RequestDescription::getSkipCount will give |
343
|
|
|
* non-null value. If $top is specified then |
344
|
|
|
* RequestDescription::getTopCount will give non-null value. |
345
|
|
|
* 2. If server side paging is enabled for the requested resource |
346
|
|
|
* If server-side paging is enabled for the requested resource then |
347
|
|
|
* RequestDescription::getTopCount will give non-null value. |
348
|
|
|
* |
349
|
|
|
*/ |
350
|
|
|
|
351
|
|
|
if (null !== $this->request->getSkipCount() || null !== $this->request->getTopCount()) { |
352
|
|
|
$orderBy = null !== $orderBy ? $orderBy . ', ' : null; |
353
|
|
|
$keys = array_keys($targetResourceType->getKeyProperties()); |
354
|
|
|
//assert(!empty($keys)) |
355
|
|
|
foreach ($keys as $key) { |
356
|
|
|
$orderBy = $orderBy . $key . ', '; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
$orderBy = rtrim(strval($orderBy), ', '); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
if (null !== $orderBy && '' != trim($orderBy)) { |
363
|
|
|
$setWrapper = $this->request->getTargetResourceSetWrapper(); |
364
|
|
|
assert(null != $setWrapper, 'Target resource set wrapper must not be null'); |
365
|
|
|
$internalOrderByInfo = OrderByParser::parseOrderByClause( |
366
|
|
|
$setWrapper, |
367
|
|
|
$targetResourceType, |
368
|
|
|
$orderBy, |
369
|
|
|
$this->service->getProvidersWrapper() |
370
|
|
|
); |
371
|
|
|
|
372
|
|
|
$this->request->setInternalOrderByInfo( |
373
|
|
|
$internalOrderByInfo |
374
|
|
|
); |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Process the $filter option in the request and update request description. |
380
|
|
|
* |
381
|
|
|
* |
382
|
|
|
* @throws ODataException Throws error in the following cases: |
383
|
|
|
* (1) If $filter cannot be applied to the |
384
|
|
|
* resource targeted by the request uri |
385
|
|
|
* (2) If any error occurred while parsing and |
386
|
|
|
* translating the odata $filter expression |
387
|
|
|
* to expression tree |
388
|
|
|
* (3) If any error occurred while generating |
389
|
|
|
* php expression from expression tree |
390
|
|
|
* @throws NotImplementedException |
391
|
|
|
* @throws ReflectionException |
392
|
|
|
*/ |
393
|
|
|
private function processFilter(): void |
394
|
|
|
{ |
395
|
|
|
$filter = $this->service->getHost()->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FILTER); |
396
|
|
|
if (null === $filter) { |
397
|
|
|
return; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
$kind = $this->request->getTargetKind(); |
401
|
|
|
if (!($kind->isNotFilterable() |
402
|
|
|
|| $this->request->queryType == QueryType::COUNT()) |
403
|
|
|
) { |
404
|
|
|
throw ODataException::createBadRequestError( |
405
|
|
|
Messages::queryProcessorQueryFilterOptionNotApplicable() |
406
|
|
|
); |
407
|
|
|
} |
408
|
|
|
$resourceType = $this->request->getTargetResourceType(); |
409
|
|
|
$expressionProvider = $this->service->getProvidersWrapper()->getExpressionProvider(); |
410
|
|
|
$filterInfo = ExpressionParser2::parseExpression2($filter, $resourceType, $expressionProvider); |
411
|
|
|
$this->request->setFilterInfo($filterInfo); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* Process the $inlinecount option and update the request description. |
416
|
|
|
* |
417
|
|
|
* |
418
|
|
|
* @throws ODataException Throws bad request error in the following cases |
419
|
|
|
* (1) If $inlinecount is disabled by the developer |
420
|
|
|
* (2) If both $count and $inlinecount specified |
421
|
|
|
* (3) If $inlinecount value is unknown |
422
|
|
|
* (4) If capability negotiation over version fails |
423
|
|
|
*/ |
424
|
|
|
private function processCount(): void |
425
|
|
|
{ |
426
|
|
|
$inlineCount = $this->service->getHost()->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_INLINECOUNT); |
427
|
|
|
|
428
|
|
|
//If it's not specified, we're done |
429
|
|
|
if (null === $inlineCount) { |
430
|
|
|
return; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
//If the service doesn't allow count requests..then throw an exception |
434
|
|
|
if (!$this->service->getConfiguration()->getAcceptCountRequests()) { |
435
|
|
|
throw ODataException::createBadRequestError( |
436
|
|
|
Messages::configurationCountNotAccepted() |
437
|
|
|
); |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
$inlineCount = trim($inlineCount); |
441
|
|
|
|
442
|
|
|
//if it's set to none, we don't do inline counts |
443
|
|
|
if ($inlineCount === ODataConstants::URI_ROWCOUNT_OFFOPTION) { |
444
|
|
|
return; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
//You can't specify $count & $inlinecount together |
448
|
|
|
//TODO: ensure there's a test for this case see #55 |
449
|
|
|
if ($this->request->queryType == QueryType::COUNT()) { |
450
|
|
|
throw ODataException::createBadRequestError( |
451
|
|
|
Messages::queryProcessorInlineCountWithValueCount() |
452
|
|
|
); |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
$this->checkSetQueryApplicable(); //TODO: why do we do this check? |
456
|
|
|
|
457
|
|
|
if ($inlineCount === ODataConstants::URI_ROWCOUNT_ALLOPTION) { |
458
|
|
|
$this->request->queryType = QueryType::ENTITIES_WITH_COUNT(); |
459
|
|
|
|
460
|
|
|
$this->request->raiseMinVersionRequirement(2, 0); |
461
|
|
|
$this->request->raiseResponseVersion(2, 0); |
462
|
|
|
} else { |
463
|
|
|
throw ODataException::createBadRequestError( |
464
|
|
|
Messages::queryProcessorInvalidInlineCountOptionError() |
465
|
|
|
); |
466
|
|
|
} |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* Process the $skiptoken option in the request and update the request |
471
|
|
|
* description, this function requires _processOrderBy method to be |
472
|
|
|
* already invoked. |
473
|
|
|
* |
474
|
|
|
* |
475
|
|
|
* @throws ODataException Throws bad request error in the following cases |
476
|
|
|
* @throws ReflectionException |
477
|
|
|
* (1) If $skiptoken cannot be applied to the |
478
|
|
|
* resource targeted by the request uri |
479
|
|
|
* (2) If paging is not enabled for the resource |
480
|
|
|
* targeted by the request uri |
481
|
|
|
* (3) If parsing of $skiptoken fails |
482
|
|
|
* (4) If capability negotiation over version fails |
483
|
|
|
*/ |
484
|
|
|
private function processSkipToken(): void |
485
|
|
|
{ |
486
|
|
|
$skipToken = $this->service->getHost()->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_SKIPTOKEN); |
487
|
|
|
if (null === $skipToken) { |
488
|
|
|
return; |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
if (!$this->pagingApplicable) { |
492
|
|
|
throw ODataException::createBadRequestError( |
493
|
|
|
Messages::queryProcessorSkipTokenNotAllowed() |
494
|
|
|
); |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
if (!$this->isSSPagingRequired()) { |
498
|
|
|
$set = $this->request->getTargetResourceSetWrapper(); |
499
|
|
|
$setName = (null != $set) ? $set->getName() : 'null'; |
500
|
|
|
$msg = Messages::queryProcessorSkipTokenCannotBeAppliedForNonPagedResourceSet($setName); |
501
|
|
|
throw ODataException::createBadRequestError($msg); |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
$internalOrderByInfo = $this->request->getInternalOrderByInfo(); |
505
|
|
|
assert($internalOrderByInfo != null, 'Internal order info must not be null'); |
506
|
|
|
$targetResourceType = $this->request->getTargetResourceType(); |
507
|
|
|
assert($targetResourceType != null, 'Request target resource type must not be null'); |
508
|
|
|
|
509
|
|
|
$internalSkipTokenInfo = SkipTokenParser::parseSkipTokenClause( |
510
|
|
|
$targetResourceType, |
511
|
|
|
$internalOrderByInfo, |
512
|
|
|
$skipToken |
513
|
|
|
); |
514
|
|
|
$this->request->setInternalSkipTokenInfo($internalSkipTokenInfo); |
515
|
|
|
$this->request->raiseMinVersionRequirement(2, 0); |
516
|
|
|
$this->request->raiseResponseVersion(2, 0); |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
/** |
520
|
|
|
* Process the $expand and $select option and update the request description. |
521
|
|
|
* |
522
|
|
|
* |
523
|
|
|
* @throws ODataException Throws bad request error in the following cases |
524
|
|
|
* @throws InvalidOperationException |
525
|
|
|
* @throws ReflectionException |
526
|
|
|
* (1) If $expand or select cannot be applied to the |
527
|
|
|
* requested resource. |
528
|
|
|
* (2) If projection is disabled by the developer |
529
|
|
|
* (3) If some error occurs while parsing the options |
530
|
|
|
*/ |
531
|
|
|
private function processExpandAndSelect(): void |
532
|
|
|
{ |
533
|
|
|
$expand = $this->service->getHost()->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_EXPAND); |
534
|
|
|
|
535
|
|
|
if (null !== $expand) { |
536
|
|
|
$this->checkExpandOrSelectApplicable(ODataConstants::HTTPQUERY_STRING_EXPAND); |
537
|
|
|
} |
538
|
|
|
|
539
|
|
|
$select = $this->service->getHost()->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_SELECT); |
540
|
|
|
|
541
|
|
|
if (null !== $select) { |
542
|
|
|
if (!$this->service->getConfiguration()->getAcceptProjectionRequests()) { |
543
|
|
|
throw ODataException::createBadRequestError(Messages::configurationProjectionsNotAccepted()); |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
$this->checkExpandOrSelectApplicable(ODataConstants::HTTPQUERY_STRING_SELECT); |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
// We will generate RootProjectionNode in case of $link request also, but |
550
|
|
|
// expand and select in this case must be null (we are ensuring this above) |
551
|
|
|
// 'RootProjectionNode' is required while generating next page Link |
552
|
|
|
if ($this->expandSelectApplicable || $this->request->isLinkUri()) { |
553
|
|
|
$rootProjectionNode = ExpandProjectionParser::parseExpandAndSelectClause( |
554
|
|
|
$this->request->getTargetResourceSetWrapper(), |
555
|
|
|
$this->request->getTargetResourceType(), |
556
|
|
|
$this->request->getInternalOrderByInfo(), |
557
|
|
|
$this->request->getSkipCount(), |
558
|
|
|
$this->request->getTopCount(), |
559
|
|
|
$expand, |
560
|
|
|
$select, |
561
|
|
|
$this->service->getProvidersWrapper() |
562
|
|
|
); |
563
|
|
|
if ($rootProjectionNode->isSelectionSpecified()) { |
564
|
|
|
$this->request->raiseMinVersionRequirement(2, 0); |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
if ($rootProjectionNode->hasPagedExpandedResult()) { |
568
|
|
|
$this->request->raiseResponseVersion(2, 0); |
569
|
|
|
} |
570
|
|
|
$this->request->setRootProjectionNode($rootProjectionNode); |
571
|
|
|
} |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
/** |
575
|
|
|
* To check whether the the query options $select, $expand |
576
|
|
|
* is applicable for the current requested resource. |
577
|
|
|
* |
578
|
|
|
* @param string $queryItem The query option to check |
579
|
|
|
* |
580
|
|
|
* @throws ODataException Throws bad request error if the query |
581
|
|
|
* options $select, $expand cannot be |
582
|
|
|
* applied to the requested resource |
583
|
|
|
*/ |
584
|
|
|
private function checkExpandOrSelectApplicable(string $queryItem): void |
585
|
|
|
{ |
586
|
|
|
if (!$this->expandSelectApplicable) { |
587
|
|
|
throw ODataException::createBadRequestError( |
588
|
|
|
Messages::queryProcessorSelectOrExpandOptionNotApplicable($queryItem) |
589
|
|
|
); |
590
|
|
|
} |
591
|
|
|
} |
592
|
|
|
} |
593
|
|
|
|