Completed
Push — master ( be1bdd...f7ce19 )
by Alex
21s queued 18s
created

generateTopLevelComparisonFunction()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 2
nop 0
dl 0
loc 17
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\UriProcessor\QueryProcessor\OrderByParser;
6
7
use POData\Common\InvalidOperationException;
8
use POData\Common\Messages;
9
use POData\Common\ODataException;
10
use POData\Common\ReflectionHandler;
11
use POData\Providers\Metadata\ResourceEntityType;
12
use POData\Providers\Metadata\ResourceProperty;
13
use POData\Providers\Metadata\ResourcePropertyKind;
14
use POData\Providers\Metadata\ResourceSetWrapper;
15
use POData\Providers\Metadata\ResourceType;
16
use POData\Providers\Metadata\Type\Binary;
17
use POData\Providers\ProvidersWrapper;
18
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
19
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
20
use ReflectionClass;
21
22
/**
23
 * Class OrderByParser.
24
 *
25
 * Class to parse $orderby query option and perform syntax validation
26
 * and build 'OrderBy Tree' along with next level of validation, the
27
 * created tree is used for building sort functions and 'OrderByInfo' structure.
28
 *
29
 * The syntax of orderby clause is:
30
 *
31
 * OrderByClause         : OrderByPathSegment [, OrderByPathSegment]*
32
 * OrderByPathSegment    : OrderBySubPathSegment[/OrderBySubPathSegment]*[asc|desc]?
33
 * OrderBySubPathSegment : identifier
34
 */
35
class OrderByParser
36
{
37
    /**
38
     * Collection of anonymous sorter function corresponding to each orderby path segment.
39
     *
40
     * @var callable[]
41
     */
42
    private $comparisonFunctions = [];
43
44
    /**
45
     * The top level sorter function generated from orderby path segments.
46
     *
47
     * @var callable
48
     */
49
    private $topLevelComparisonFunction;
50
51
    /**
52
     * The structure holds information about the navigation properties
53
     * used in the orderby clause (if any), and orderby path if IDSQP
54
     * implementor want to perform sorting.
55
     *
56
     * @var OrderByInfo
57
     */
58
    private $orderByInfo;
59
60
    /**
61
     * Reference to metadata and query provider wrapper.
62
     *
63
     * @var ProvidersWrapper
64
     */
65
    private $providerWrapper;
66
67
    /**
68
     * This object will be of type of the resource set identified by the request uri.
69
     *
70
     * @var mixed
71
     */
72
    private $dummyObject;
73
74
    /**
75
     * Root node for tree ordering.
76
     *
77
     * @var OrderByNode
78
     */
79
    private $rootOrderByNode;
80
81
    /**
82
     * Creates new instance of OrderByParser.
83
     *
84
     * @param ProvidersWrapper $providerWrapper Reference to metadata
85
     *                                          and query provider
86
     *                                          wrapper
87
     */
88
    private function __construct(ProvidersWrapper $providerWrapper)
89
    {
90
        $this->providerWrapper = $providerWrapper;
91
    }
92
93
    /**
94
     * This function perform the following tasks with the help of internal helper
95
     * functions
96
     * (1) Read the orderby clause and perform basic syntax checks
97
     * (2) Build 'Order By Tree', creates anonymous sorter function for each leaf node and check for error
98
     * (3) Build 'OrderInfo' structure, holds information about the navigation
99
     *     properties used in the orderby clause (if any) and orderby path if
100
     *     IDSQP implementor want to perform sorting
101
     * (4) Build top level anonymous sorter function
102
     * (4) Release resources hold by the 'Order By Tree'
103
     * (5) Create 'InternalOrderInfo' structure, which wraps 'OrderInfo' and top
104
     *     level sorter function.
105
     *
106
     * @param ResourceSetWrapper $resourceSetWrapper ResourceSetWrapper for the resource targeted by resource path
107
     * @param ResourceType       $resourceType       ResourceType for the resource targeted by resource path
108
     * @param string             $orderBy            The orderby clause
109
     * @param ProvidersWrapper   $providerWrapper    Reference to the wrapper for IDSQP and IDSMP impl
110
     *
111
     * @throws ODataException            If any error occur while parsing orderby clause
112
     * @throws InvalidOperationException
113
     * @throws \ReflectionException
114
     *
115
     * @return InternalOrderByInfo
116
     */
117
    public static function parseOrderByClause(
118
        ResourceSetWrapper $resourceSetWrapper,
119
        ResourceType $resourceType,
120
        $orderBy,
121
        ProvidersWrapper $providerWrapper
122
    ) {
123
        assert(is_string($orderBy), 'OrderBy clause must be a string');
124
        $orderBy = trim($orderBy);
125
        if (0 == strlen($orderBy)) {
126
            throw new InvalidOperationException('OrderBy clause must not be trimmable to an empty string');
127
        }
128
        $orderByParser = new self($providerWrapper);
129
        try {
130
            $instance = $resourceType->getInstanceType();
131
            assert($instance instanceof ReflectionClass, get_class($instance));
132
            $orderByParser->dummyObject = $instance->newInstanceArgs([]);
133
        } catch (\ReflectionException $reflectionException) {
134
            throw ODataException::createInternalServerError(Messages::orderByParserFailedToCreateDummyObject());
135
        }
136
        $orderByParser->rootOrderByNode = new OrderByRootNode($resourceSetWrapper, $resourceType);
137
        $orderByPathSegments            = $orderByParser->readOrderBy($orderBy);
138
139
        $orderByParser->buildOrderByTree($orderByPathSegments);
140
        $orderByParser->createOrderInfo($orderByPathSegments);
141
        $orderByParser->generateTopLevelComparisonFunction();
142
        //Recursively release the resources
143
        $orderByParser->rootOrderByNode->free();
144
        //creates internal order info wrapper
145
        $internalOrderInfo = new InternalOrderByInfo(
146
            $orderByParser->orderByInfo,
147
            $orderByParser->comparisonFunctions,
148
            $orderByParser->topLevelComparisonFunction,
149
            $orderByParser->dummyObject,
150
            $resourceType
151
        );
152
        unset($orderByParser->orderByInfo);
153
        unset($orderByParser->topLevelComparisonFunction);
154
155
        return $internalOrderInfo;
156
    }
157
158
    /**
159
     * Build 'OrderBy Tree' from the given orderby path segments, also build
160
     * comparison function for each path segment.
161
     *
162
     * @param array(array) &$orderByPathSegments Collection of orderby path segments,
163
     *                                           this is passed by reference
164
     *                                           since we need this function to
165
     *                                           modify this array in two cases:
166
     *                                           1. if asc or desc present, then the
167
     *                                           corresponding sub path segment
168
     *                                           should be removed
169
     *                                           2. remove duplicate orderby path
170
     *                                           segment
171
     *
172
     * @throws ODataException       If any error occurs while processing the orderby path segments
173
     * @throws \ReflectionException
174
     * @return mixed
175
     */
176
    private function buildOrderByTree(&$orderByPathSegments)
177
    {
178
        foreach ($orderByPathSegments as $index1 => &$orderBySubPathSegments) {
179
            /** @var OrderByNode $currentNode */
180
            $currentNode   = $this->rootOrderByNode;
181
            $currentObject = $this->dummyObject;
182
            $ascending     = true;
183
            $subPathCount  = count($orderBySubPathSegments);
184
            // Check sort order is specified in the path, if so set a
185
            // flag and remove that segment
186
            if ($subPathCount > 1) {
187
                if ('*desc' === $orderBySubPathSegments[$subPathCount - 1]) {
188
                    $ascending = false;
189
                    unset($orderBySubPathSegments[$subPathCount - 1]);
190
                    --$subPathCount;
191
                } elseif ('*asc' === $orderBySubPathSegments[$subPathCount - 1]) {
192
                    unset($orderBySubPathSegments[$subPathCount - 1]);
193
                    --$subPathCount;
194
                }
195
            }
196
197
            $ancestors = [$this->rootOrderByNode->getResourceSetWrapper()->getName()];
198
            foreach ($orderBySubPathSegments as $index2 => $orderBySubPathSegment) {
199
                $isLastSegment      = ($index2 == $subPathCount - 1);
200
                $resourceSetWrapper = null;
201
                /** @var ResourceEntityType $resourceType */
202
                $resourceType = $currentNode->getResourceType();
203
                /** @var ResourceProperty $resourceProperty */
204
                $resourceProperty = $resourceType->resolveProperty($orderBySubPathSegment);
205
                if (null === $resourceProperty) {
206
                    throw ODataException::createSyntaxError(
207
                        Messages::orderByParserPropertyNotFound(
208
                            $resourceType->getFullName(),
209
                            $orderBySubPathSegment
210
                        )
211
                    );
212
                }
213
                /** @var ResourcePropertyKind $rKind */
214
                $rKind = $resourceProperty->getKind();
215
                $rawKind = ($rKind instanceof ResourcePropertyKind) ? $rKind->getValue() : $rKind;
216
217
                if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG())) {
218
                    throw ODataException::createBadRequestError(
219
                        Messages::orderByParserBagPropertyNotAllowed(
220
                            $resourceProperty->getName()
221
                        )
222
                    );
223
                } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE())) {
224
                    if (!$isLastSegment) {
225
                        throw ODataException::createBadRequestError(
226
                            Messages::orderByParserPrimitiveAsIntermediateSegment(
227
                                $resourceProperty->getName()
228
                            )
229
                        );
230
                    }
231
232
                    $type = $resourceProperty->getInstanceType();
233
                    if ($type instanceof Binary) {
234
                        throw ODataException::createBadRequestError(
235
                            Messages::orderByParserSortByBinaryPropertyNotAllowed($resourceProperty->getName())
236
                        );
237
                    }
238
                } elseif ($rawKind == ResourcePropertyKind::RESOURCESET_REFERENCE
239
                    || $rawKind == ResourcePropertyKind::RESOURCE_REFERENCE
240
                ) {
241
                    $this->assertion($currentNode instanceof OrderByRootNode || $currentNode instanceof OrderByNode);
242
                    $resourceSetWrapper = $currentNode->getResourceSetWrapper();
243
                    $this->assertion(null !== $resourceSetWrapper);
244
                    $resourceSetWrapper
245
                        = $this->providerWrapper->getResourceSetWrapperForNavigationProperty(
246
                            $resourceSetWrapper,
247
                            $resourceType,
248
                            $resourceProperty
249
                        );
250
                    if (null === $resourceSetWrapper) {
251
                        throw ODataException::createBadRequestError(
252
                            Messages::badRequestInvalidPropertyNameSpecified(
253
                                $resourceType->getFullName(),
254
                                $orderBySubPathSegment
255
                            )
256
                        );
257
                    }
258
259
                    if ($rKind == ResourcePropertyKind::RESOURCESET_REFERENCE()) {
260
                        throw ODataException::createBadRequestError(
261
                            Messages::orderByParserResourceSetReferenceNotAllowed(
262
                                $resourceProperty->getName(),
263
                                $resourceType->getFullName()
264
                            )
265
                        );
266
                    }
267
268
                    $resourceSetWrapper->checkResourceSetRightsForRead(true);
269
                    if ($isLastSegment) {
270
                        throw ODataException::createBadRequestError(
271
                            Messages::orderByParserSortByNavigationPropertyIsNotAllowed(
272
                                $resourceProperty->getName()
273
                            )
274
                        );
275
                    }
276
277
                    $ancestors[] = $orderBySubPathSegment;
278
                } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE())) {
279
                    if ($isLastSegment) {
280
                        throw ODataException::createBadRequestError(
281
                            Messages::orderByParserSortByComplexPropertyIsNotAllowed(
282
                                $resourceProperty->getName()
283
                            )
284
                        );
285
                    }
286
287
                    $ancestors[] = $orderBySubPathSegment;
288
                } else {
289
                    throw ODataException::createInternalServerError(
290
                        Messages::orderByParserUnexpectedPropertyType()
291
                    );
292
                }
293
294
                $node = $currentNode->findNode($orderBySubPathSegment);
295
                if (null === $node) {
296
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE())) {
297
                        $node = new OrderByLeafNode(
298
                            $orderBySubPathSegment,
299
                            $resourceProperty,
300
                            $ascending
301
                        );
302
                        $this->comparisonFunctions[] = $node->buildComparisonFunction($ancestors);
303
                    } elseif ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE()) {
304
                        $node = new OrderByNode(
305
                            $orderBySubPathSegment,
306
                            $resourceProperty,
307
                            $resourceSetWrapper
308
                        );
309
                        // Initialize this member variable (identified by
310
                        // $resourceProperty) of parent object.
311
                        try {
312
                            $instance = $resourceProperty->getInstanceType();
313
                            assert($instance instanceof ReflectionClass, get_class($instance));
314
                            $object = $instance->newInstanceArgs();
315
                            $resourceType->setPropertyValue($currentObject, $resourceProperty->getName(), $object);
316
                            $currentObject = $object;
317
                        } catch (\ReflectionException $reflectionException) {
318
                            $this->throwBadAccessOrInitException($resourceProperty, $resourceType);
319
                        }
320
                    } elseif ($resourceProperty->getKind() == ResourcePropertyKind::COMPLEX_TYPE()) {
321
                        $node = new OrderByNode($orderBySubPathSegment, $resourceProperty, null);
322
                        // Initialize this member variable
323
                        // (identified by $resourceProperty)of parent object.
324
                        try {
325
                            $instance = $resourceProperty->getInstanceType();
326
                            assert($instance instanceof ReflectionClass, get_class($instance));
327
                            $object = $instance->newInstanceArgs();
328
                            $resourceType->setPropertyValue($currentObject, $resourceProperty->getName(), $object);
329
                            $currentObject = $object;
330
                        } catch (\ReflectionException $reflectionException) {
331
                            $this->throwBadAccessOrInitException($resourceProperty, $resourceType);
332
                        }
333
                    }
334
335
                    $currentNode->addNode($node);
336
                } else {
337
                    try {
338
                        $currentObject = ReflectionHandler::getProperty($currentObject, $resourceProperty->getName());
339
                    } catch (\ReflectionException $reflectionException) {
340
                        $this->throwBadAccessOrInitException($resourceProperty, $resourceType);
341
                    }
342
343
                    if ($node instanceof OrderByLeafNode) {
344
                        //remove duplicate orderby path
345
                        unset($orderByPathSegments[$index1]);
346
                    }
347
                }
348
349
                $currentNode = $node;
350
            }
351
        }
352
        return null;
353
    }
354
355
    /**
356
     * Traverse 'Order By Tree' and create 'OrderInfo' structure.
357
     *
358
     * @param array<array> $orderByPaths The orderby paths
359
     *
360
     * @throws ODataException If parser finds an inconsistent-tree state, throws unexpected state error
361
     *
362
     * @return void
363
     */
364
    private function createOrderInfo(array $orderByPaths): void
365
    {
366
        $orderByPathSegments           = [];
367
        $navigationPropertiesInThePath = [];
368
        foreach ($orderByPaths as $index => $orderBySubPaths) {
369
            /** @var OrderByNode $currentNode */
370
            $currentNode            = $this->rootOrderByNode;
371
            $orderBySubPathSegments = [];
372
            foreach ($orderBySubPaths as $orderBySubPath) {
373
                /** @var OrderByNode $node */
374
                $node = $currentNode->findNode($orderBySubPath);
375
                $this->assertion(null !== $node);
376
                $resourceProperty = $node->getResourceProperty();
377
                if ($node instanceof OrderByNode && null !== $node->getResourceSetWrapper()) {
378
                    if (!array_key_exists($index, $navigationPropertiesInThePath)) {
379
                        $navigationPropertiesInThePath[$index] = [];
380
                    }
381
382
                    $navigationPropertiesInThePath[$index][] = $resourceProperty;
383
                }
384
385
                $orderBySubPathSegments[] = new OrderBySubPathSegment($resourceProperty);
386
                /** @var OrderByLeafNode $currentNode */
387
                $currentNode = $node;
388
            }
389
390
            $this->assertion($currentNode instanceof OrderByLeafNode);
391
            /** @var OrderByLeafNode $newNode */
392
            $newNode               = $currentNode;
393
            $orderByPathSegments[] = new OrderByPathSegment($orderBySubPathSegments, $newNode->isAscending());
394
            unset($orderBySubPathSegments);
395
        }
396
397
        $this->orderByInfo = new OrderByInfo(
398
            $orderByPathSegments,
399
            empty($navigationPropertiesInThePath) ? null : $navigationPropertiesInThePath
400
        );
401
    }
402
403
    /**
404
     * Generates top level comparison function from sub comparison functions.
405
     * @throws ODataException
406
     */
407
    private function generateTopLevelComparisonFunction()
408
    {
409
        $comparisonFunctionCount = count($this->comparisonFunctions);
410
        $this->assertion(0 < $comparisonFunctionCount);
411
        if (1 == $comparisonFunctionCount) {
412
            $this->topLevelComparisonFunction = $this->comparisonFunctions[0];
413
        } else {
414
            $funcList                         = $this->comparisonFunctions;
415
            $this->topLevelComparisonFunction = function ($object1, $object2) use ($funcList) {
416
                $ret = 0;
417
                foreach ($funcList as $f) {
418
                    $ret = $f($object1, $object2);
419
                    if (0 != $ret) {
420
                        return $ret;
421
                    }
422
                }
423
                return $ret;
424
            };
425
        }
426
    }
427
428
    /**
429
     * Read orderby clause.
430
     *
431
     * @param string $value orderby clause to read
432
     *
433
     * @throws ODataException If any syntax error found while reading the clause
434
     *
435
     * @return array<array> An array of 'OrderByPathSegment's, each of which
436
     *                      is array of 'OrderBySubPathSegment's
437
     */
438
    private function readOrderBy($value)
439
    {
440
        $orderByPathSegments = [];
441
        $lexer               = new ExpressionLexer($value);
442
        $i                   = 0;
443
        while ($lexer->getCurrentToken()->getId() != ExpressionTokenId::END()) {
444
            $orderBySubPathSegment = $lexer->readDottedIdentifier();
445
            if (!array_key_exists($i, $orderByPathSegments)) {
446
                $orderByPathSegments[$i] = [];
447
            }
448
449
            $orderByPathSegments[$i][] = $orderBySubPathSegment;
450
            $tokenId                   = $lexer->getCurrentToken()->getId();
451
            if ($tokenId != ExpressionTokenId::END()) {
452
                if ($tokenId != ExpressionTokenId::SLASH()) {
453
                    if ($tokenId != ExpressionTokenId::COMMA()) {
454
                        $lexer->validateToken(ExpressionTokenId::IDENTIFIER());
455
                        $identifier = $lexer->getCurrentToken()->Text;
456
                        if ($identifier !== 'asc' && $identifier !== 'desc') {
457
                            // force lexer to throw syntax error as we found
458
                            // unexpected identifier
459
                            $lexer->validateToken(ExpressionTokenId::DOT());
460
                        }
461
462
                        $orderByPathSegments[$i][] = '*' . $identifier;
463
                        $lexer->nextToken();
464
                        $tokenId = $lexer->getCurrentToken()->getId();
465
                        if ($tokenId != ExpressionTokenId::END()) {
466
                            $lexer->validateToken(ExpressionTokenId::COMMA());
467
                            ++$i;
468
                        }
469
                    } else {
470
                        ++$i;
471
                    }
472
                }
473
474
                $lexer->nextToken();
475
            }
476
        }
477
478
        return $orderByPathSegments;
479
    }
480
481
    /**
482
     * Assert that the given condition is true, if false throw ODataException for unexpected state.
483
     *
484
     * @param bool $condition The condition to assert
485
     *
486
     * @throws ODataException
487
     */
488
    private function assertion($condition)
489
    {
490
        if (!$condition) {
491
            throw ODataException::createInternalServerError(Messages::orderByParserUnExpectedState());
492
        }
493
    }
494
495
    /**
496
     * @param ResourceProperty $resourceProperty
497
     * @param ResourceType $resourceType
498
     * @throws ODataException
499
     */
500
    private function throwBadAccessOrInitException(
501
        ResourceProperty $resourceProperty,
502
        ResourceType $resourceType
503
    ): void {
504
        throw ODataException::createInternalServerError(
505
            Messages::orderByParserFailedToAccessOrInitializeProperty(
506
                $resourceProperty->getName(),
507
                $resourceType->getName()
508
            )
509
        );
510
    }
511
}
512