Passed
Branch master (950424)
by Christopher
11:06
created

ExpandProjectionParser::parseExpand()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor\ExpandProjectionParser;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataException;
7
use POData\Providers\Metadata\ResourceAssociationSet;
8
use POData\Providers\Metadata\ResourceEntityType;
9
use POData\Providers\Metadata\ResourceProperty;
10
use POData\Providers\Metadata\ResourcePropertyKind;
11
use POData\Providers\Metadata\ResourceSet;
12
use POData\Providers\Metadata\ResourceSetWrapper;
13
use POData\Providers\Metadata\ResourceType;
14
use POData\Providers\Metadata\ResourceTypeKind;
15
use POData\Providers\ProvidersWrapper;
16
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
17
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
18
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
19
use POData\UriProcessor\QueryProcessor\OrderByParser\OrderByParser;
20
21
/**
22
 * Class ExpandProjectionParser.
23
 *
24
 * Class used to parse and validate $expand and $select query options and
25
 * create a 'Projection Tree' from these options, Syntax of the clause is:
26
 *
27
 * ExpandOrSelectPath : PathSegment [, PathSegment]
28
 * PathSegment        : SubPathSegment [\ SubPathSegment]
29
 * SubPathSegment     : DottedIdentifier
30
 * SubPathSegment     : * (Only if the SubPathSegment is last segment and
31
 *                      belongs to select path)
32
 * DottedIdentifier   : Identifier [. Identifier]
33
 * Identifier         : NavigationProperty
34
 * Identifier         : NonNavigationProperty (Only if if the SubPathSegment
35
 *                      is last segment and belongs to select path)
36
 */
37
class ExpandProjectionParser
38
{
39
    /**
40
     * The wrapper of IMetadataProvider and IQueryProvider
41
     * .
42
     *
43
     * @var ProvidersWrapper
44
     */
45
    private $providerWrapper;
46
47
    /**
48
     * Holds reference to the root of 'Projection Tree'.
49
     *
50
     * @var RootProjectionNode
51
     */
52
    private $rootProjectionNode;
53
54
    /**
55
     * Creates new instance of ExpandProjectionParser.
56
     *
57
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
58
     */
59
    private function __construct(ProvidersWrapper $providerWrapper)
60
    {
61
        $this->providerWrapper = $providerWrapper;
62
    }
63
64
    /**
65
     * Parse the given expand and select clause, validate them
66
     * and build 'Projection Tree'.
67
     *
68
     * @param ResourceSetWrapper  $resourceSetWrapper The resource set identified by the resource path uri
69
     * @param ResourceType        $resourceType       The resource type of entities identified by the resource path uri
70
     * @param InternalOrderByInfo $internalOrderInfo  The top level sort information, this will be set if the $skip,
71
     *                                                $top is specified in the
72
     *                                                request uri or Server side paging is
73
     *                                                enabled for top level resource
74
     * @param int                 $skipCount          The value of $skip option applied to the top level resource
75
     *                                                set identified by the
76
     *                                                resource path uri
77
     *                                                null means $skip
78
     *                                                option is not present
79
     * @param int                 $takeCount          The minimum among the value of $top option applied to and
80
     *                                                page size configured
81
     *                                                for the top level
82
     *                                                resource
83
     *                                                set identified
84
     *                                                by the resource
85
     *                                                path uri.
86
     *                                                null means $top option
87
     *                                                is not present and/or
88
     *                                                page size is not
89
     *                                                configured for top
90
     *                                                level resource set
91
     * @param string              $expand             The value of $expand clause
92
     * @param string              $select             The value of $select clause
93
     * @param ProvidersWrapper    $providerWrapper    Reference to metadata and query provider wrapper
94
     *
95
     * @throws ODataException If any error occur while parsing expand and/or select clause
96
     *
97
     * @return RootProjectionNode Returns root of the 'Projection Tree'
98
     */
99
    public static function parseExpandAndSelectClause(
100
        ResourceSetWrapper $resourceSetWrapper,
101
        ResourceType $resourceType,
102
        $internalOrderInfo,
103
        $skipCount,
104
        $takeCount,
105
        $expand,
106
        $select,
107
        ProvidersWrapper $providerWrapper
108
    ) {
109
        $parser = new self($providerWrapper);
110
        $parser->rootProjectionNode = new RootProjectionNode(
111
            $resourceSetWrapper,
112
            $internalOrderInfo,
113
            $skipCount,
114
            $takeCount,
115
            null,
116
            $resourceType
117
        );
118
        $parser->parseExpand($expand);
119
        $parser->parseSelect($select);
120
121
        return $parser->rootProjectionNode;
122
    }
123
124
    /**
125
     * Read the given expand clause and build 'Projection Tree',
126
     * do nothing if the clause is null.
127
     *
128
     * @param string $expand Value of $expand clause
129
     *
130
     * @throws ODataException If any error occurs while reading expand clause
131
     *                        or building the projection tree
132
     */
133
    private function parseExpand($expand)
134
    {
135
        if (null !== $expand) {
136
            $pathSegments = $this->readExpandOrSelect($expand, false);
137
            $this->buildProjectionTree($pathSegments);
138
            $this->rootProjectionNode->setExpansionSpecified();
139
        }
140
    }
141
142
    /**
143
     * Read the given select clause and apply selection to the
144
     * 'Projection Tree', mark the entire tree as selected if this
145
     * clause is null
146
     * Note: _parseExpand should to be called before the invocation
147
     * of this function so that basic 'Projection Tree' with expand
148
     * information will be ready.
149
     *
150
     * @param string $select Value of $select clause
151
     *
152
     * @throws ODataException If any error occurs while reading expand clause
153
     *                        or applying selection to projection tree
154
     */
155
    private function parseSelect($select)
156
    {
157
        if (null === $select) {
158
            $this->rootProjectionNode->markSubtreeAsSelected();
159
        } else {
160
            $pathSegments = $this->readExpandOrSelect($select, true);
161
            $this->applySelectionToProjectionTree($pathSegments);
162
            $this->rootProjectionNode->setSelectionSpecified();
163
            $this->rootProjectionNode->removeNonSelectedNodes();
164
            $this->rootProjectionNode->removeNodesAlreadyIncludedImplicitly();
165
            //TODO: Move sort to parseExpandAndSelectClause function
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
166
            $this->rootProjectionNode->sortNodes();
167
        }
168
    }
169
170
    /**
171
     * Build 'Projection Tree' from the given expand path segments.
172
     *
173
     * @param array<array> $expandPathSegments Collection of expand paths
174
     *
175
     * @throws ODataException If any error occurs while processing the expand path segments
176
     */
177
    private function buildProjectionTree($expandPathSegments)
178
    {
179
        foreach ($expandPathSegments as $expandSubPathSegments) {
180
            $currentNode = $this->rootProjectionNode;
181
            foreach ($expandSubPathSegments as $expandSubPathSegment) {
182
                $resourceSetWrapper = $currentNode->getResourceSetWrapper();
0 ignored issues
show
Bug introduced by
The method getResourceSetWrapper() does not exist on POData\UriProcessor\Quer...onParser\ProjectionNode. Did you maybe mean getResourceProperty()? ( Ignorable by Annotation )

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

182
                /** @scrutinizer ignore-call */ 
183
                $resourceSetWrapper = $currentNode->getResourceSetWrapper();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
183
                $resourceType = $currentNode->getResourceType();
0 ignored issues
show
Bug introduced by
The method getResourceType() does not exist on POData\UriProcessor\Quer...onParser\ProjectionNode. Did you maybe mean getResourceProperty()? ( Ignorable by Annotation )

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

183
                /** @scrutinizer ignore-call */ 
184
                $resourceType = $currentNode->getResourceType();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
184
                assert($resourceType instanceof ResourceEntityType);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

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

184
                /** @scrutinizer ignore-call */ 
185
                assert($resourceType instanceof ResourceEntityType);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
185
                $resourceProperty = $resourceType->resolveProperty($expandSubPathSegment);
186
                assert($resourceProperty instanceof ResourceProperty);
187
                $keyType = ResourceAssociationSet::keyNameFromTypeAndProperty($resourceType, $resourceProperty);
188
                $assoc = $this->getProviderWrapper()->getMetaProvider()->resolveAssociationSet($keyType);
0 ignored issues
show
Bug introduced by
The method resolveAssociationSet() does not exist on POData\Providers\Metadata\IMetadataProvider. Since it exists in all sub-types, consider adding an abstract or default implementation to POData\Providers\Metadata\IMetadataProvider. ( Ignorable by Annotation )

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

188
                $assoc = $this->getProviderWrapper()->getMetaProvider()->/** @scrutinizer ignore-call */ resolveAssociationSet($keyType);
Loading history...
189
                $concreteType = isset($assoc) ? $assoc->getEnd2()->getConcreteType() : $resourceType;
190
                if (null === $resourceProperty) {
191
                    throw ODataException::createSyntaxError(
192
                        Messages::expandProjectionParserPropertyNotFound(
193
                            $resourceType->getFullName(),
194
                            $expandSubPathSegment,
195
                            false
196
                        )
197
                    );
198
                } elseif ($resourceProperty->getTypeKind() != ResourceTypeKind::ENTITY()) {
199
                    throw ODataException::createBadRequestError(
200
                        Messages::expandProjectionParserExpandCanOnlyAppliedToEntity(
201
                            $resourceType->getFullName(),
202
                            $expandSubPathSegment
203
                        )
204
                    );
205
                }
206
                assert($resourceType instanceof ResourceEntityType);
207
208
                $resourceSetWrapper = $this->providerWrapper
209
                    ->getResourceSetWrapperForNavigationProperty(
210
                        $resourceSetWrapper,
211
                        $resourceType,
212
                        $resourceProperty
213
                    );
214
215
                if (null === $resourceSetWrapper) {
216
                    throw ODataException::createBadRequestError(
217
                        Messages::badRequestInvalidPropertyNameSpecified(
218
                            $resourceType->getFullName(),
219
                            $expandSubPathSegment
220
                        )
221
                    );
222
                }
223
224
                $singleResult
225
                    = $resourceProperty->isKindOf(
226
                        ResourcePropertyKind::RESOURCE_REFERENCE
0 ignored issues
show
Bug introduced by
POData\Providers\Metadat...ind::RESOURCE_REFERENCE of type integer is incompatible with the type POData\Providers\Metadata\ResourcePropertyKind expected by parameter $kind of POData\Providers\Metadat...rceProperty::isKindOf(). ( Ignorable by Annotation )

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

226
                        /** @scrutinizer ignore-type */ ResourcePropertyKind::RESOURCE_REFERENCE
Loading history...
227
                    );
228
                $resourceSetWrapper->checkResourceSetRightsForRead($singleResult);
229
                $pageSize = $resourceSetWrapper->getResourceSetPageSize();
230
                $internalOrderByInfo = null;
231
                if ($pageSize != 0 && !$singleResult) {
232
                    $this->rootProjectionNode->setPagedExpandedResult(true);
233
                    $rt = $resourceSetWrapper->getResourceType();
234
                    $payloadType = $rt->isAbstract() ? $concreteType : $rt;
235
236
                    $keys = array_keys($rt->getKeyProperties());
237
                    $orderBy = null;
238
                    foreach ($keys as $key) {
239
                        $orderBy = $orderBy . $key . ', ';
240
                    }
241
242
                    $orderBy = rtrim($orderBy, ', ');
243
                    $internalOrderByInfo = OrderByParser::parseOrderByClause(
244
                        $resourceSetWrapper,
245
                        $payloadType,
246
                        $orderBy,
247
                        $this->providerWrapper
248
                    );
249
                }
250
251
                $node = $currentNode->findNode($expandSubPathSegment);
0 ignored issues
show
introduced by
The method findNode() does not exist on POData\UriProcessor\Quer...onParser\ProjectionNode. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

251
                /** @scrutinizer ignore-call */ 
252
                $node = $currentNode->findNode($expandSubPathSegment);
Loading history...
252
                if (null === $node) {
253
                    $maxResultCount = $this->providerWrapper
254
                        ->getConfiguration()->getMaxResultsPerCollection();
255
                    $node = new ExpandedProjectionNode(
256
                        $expandSubPathSegment,
257
                        $resourceSetWrapper,
258
                        $internalOrderByInfo,
259
                        null,
260
                        $pageSize == 0 ? null : $pageSize,
261
                        $maxResultCount == PHP_INT_MAX ? null : $maxResultCount,
262
                        $resourceProperty
263
                    );
264
                    $currentNode->addNode($node);
0 ignored issues
show
introduced by
The method addNode() does not exist on POData\UriProcessor\Quer...onParser\ProjectionNode. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

264
                    $currentNode->/** @scrutinizer ignore-call */ 
265
                                  addNode($node);
Loading history...
265
                }
266
267
                $currentNode = $node;
268
            }
269
        }
270
    }
271
272
    /**
273
     * Modify the 'Projection Tree' to include selection details.
274
     *
275
     * @param array<array<string>> $selectPathSegments Collection of select
276
     *                                                 paths
277
     *
278
     * @throws ODataException If any error occurs while processing select
279
     *                        path segments
280
     */
281
    private function applySelectionToProjectionTree($selectPathSegments)
282
    {
283
        foreach ($selectPathSegments as $selectSubPathSegments) {
284
            $currentNode = $this->rootProjectionNode;
285
            $subPathCount = count($selectSubPathSegments);
286
            foreach ($selectSubPathSegments as $index => $selectSubPathSegment) {
287
                if (!($currentNode instanceof RootProjectionNode)
288
                    && !($currentNode instanceof ExpandedProjectionNode)
289
                ) {
290
                    throw ODataException::createBadRequestError(
291
                        Messages::expandProjectionParserPropertyWithoutMatchingExpand(
292
                            $currentNode->getPropertyName()
293
                        )
294
                    );
295
                }
296
297
                $currentNode->setSelectionFound();
298
                $isLastSegment = ($index == $subPathCount - 1);
299
                if ($selectSubPathSegment === '*') {
300
                    $currentNode->setSelectAllImmediateProperties();
301
                    break;
302
                }
303
304
                $currentResourceType = $currentNode->getResourceType();
305
                $resourceProperty
306
                    = $currentResourceType->resolveProperty(
307
                        $selectSubPathSegment
308
                    );
309
                if (null === $resourceProperty) {
310
                    throw ODataException::createSyntaxError(
311
                        Messages::expandProjectionParserPropertyNotFound(
312
                            $currentResourceType->getFullName(),
313
                            $selectSubPathSegment,
314
                            true
315
                        )
316
                    );
317
                }
318
319
                if (!$isLastSegment) {
320
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) {
0 ignored issues
show
Bug introduced by
POData\Providers\Metadat...sourcePropertyKind::BAG of type integer is incompatible with the type POData\Providers\Metadata\ResourcePropertyKind expected by parameter $kind of POData\Providers\Metadat...rceProperty::isKindOf(). ( Ignorable by Annotation )

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

320
                    if ($resourceProperty->isKindOf(/** @scrutinizer ignore-type */ ResourcePropertyKind::BAG)) {
Loading history...
321
                        throw ODataException::createBadRequestError(
322
                            Messages::expandProjectionParserBagPropertyAsInnerSelectSegment(
323
                                $currentResourceType->getFullName(),
324
                                $selectSubPathSegment
325
                            )
326
                        );
327
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
328
                        throw ODataException::createBadRequestError(
329
                            Messages::expandProjectionParserPrimitivePropertyUsedAsNavigationProperty(
330
                                $currentResourceType->getFullName(),
331
                                $selectSubPathSegment
332
                            )
333
                        );
334
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) {
335
                        throw ODataException::createBadRequestError(
336
                            Messages::expandProjectionParserComplexPropertyAsInnerSelectSegment(
337
                                $currentResourceType->getFullName(),
338
                                $selectSubPathSegment
339
                            )
340
                        );
341
                    } elseif ($resourceProperty->getKind() != ResourcePropertyKind::RESOURCE_REFERENCE
342
                              && $resourceProperty->getKind() != ResourcePropertyKind::RESOURCESET_REFERENCE) {
343
                        throw ODataException::createInternalServerError(
344
                            Messages::expandProjectionParserUnexpectedPropertyType()
345
                        );
346
                    }
347
                }
348
349
                $node = $currentNode->findNode($selectSubPathSegment);
350
                if (null === $node) {
351
                    if (!$isLastSegment) {
352
                        throw ODataException::createBadRequestError(
353
                            Messages::expandProjectionParserPropertyWithoutMatchingExpand(
354
                                $selectSubPathSegment
355
                            )
356
                        );
357
                    }
358
359
                    $node = new ProjectionNode($selectSubPathSegment, $resourceProperty);
360
                    $currentNode->addNode($node);
361
                }
362
363
                $currentNode = $node;
364
                if ($currentNode instanceof ExpandedProjectionNode
365
                    && $isLastSegment
366
                ) {
367
                    $currentNode->setSelectionFound();
368
                    $currentNode->markSubtreeAsSelected();
369
                }
370
            }
371
        }
372
    }
373
374
    /**
375
     * Read expand or select clause.
376
     *
377
     * @param string $value    expand or select clause to read
378
     * @param bool   $isSelect true means $value is value of select clause
379
     *                         else value of expand clause
380
     *
381
     * @return array<array> An array of 'PathSegment's, each of which is array of 'SubPathSegment's
382
     */
383
    private function readExpandOrSelect($value, $isSelect)
384
    {
385
        $pathSegments = [];
386
        $lexer = new ExpressionLexer($value);
387
        $i = 0;
388
        while ($lexer->getCurrentToken()->Id != ExpressionTokenId::END) {
389
            $lastSegment = false;
390
            if ($isSelect
391
                && $lexer->getCurrentToken()->Id == ExpressionTokenId::STAR
392
            ) {
393
                $lastSegment = true;
394
                $subPathSegment = $lexer->getCurrentToken()->Text;
395
                $lexer->nextToken();
396
            } else {
397
                $subPathSegment = $lexer->readDottedIdentifier();
398
            }
399
400
            if (!array_key_exists($i, $pathSegments)) {
401
                $pathSegments[$i] = [];
402
            }
403
404
            $pathSegments[$i][] = $subPathSegment;
405
            $tokenId = $lexer->getCurrentToken()->Id;
406
            if ($tokenId != ExpressionTokenId::END) {
407
                if ($lastSegment || $tokenId != ExpressionTokenId::SLASH) {
408
                    $lexer->validateToken(ExpressionTokenId::COMMA);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...xpressionTokenId::COMMA of type integer is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $tokenId of POData\UriProcessor\Quer...nLexer::validateToken(). ( Ignorable by Annotation )

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

408
                    $lexer->validateToken(/** @scrutinizer ignore-type */ ExpressionTokenId::COMMA);
Loading history...
409
                    ++$i;
410
                }
411
412
                $lexer->nextToken();
413
            }
414
        }
415
416
        return $pathSegments;
417
    }
418
419
    /**
420
     * @return ProvidersWrapper
421
     */
422
    public function getProviderWrapper()
423
    {
424
        return $this->providerWrapper;
425
    }
426
}
427