Completed
Push — master ( db31a7...9fef30 )
by Alex
15s queued 11s
created

SegmentParser::getProviderWrapper()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace POData\UriProcessor\ResourcePathProcessor\SegmentParser;
4
5
use Illuminate\Support\Str;
6
use POData\Common\Messages;
7
use POData\Common\ODataConstants;
8
use POData\Common\ODataException;
9
use POData\Providers\Metadata\ResourceEntityType;
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
17
/**
18
 * Class SegmentParser.
19
 *
20
 * A parser to parse the segments in OData URI, Uri is made up of bunch of segments,
21
 * each segment is separated by '/' character
22
 * e.g. Customers('ALFKI')/Orders(2134)/Order_Details/Product
23
 *
24
 * Syntax of an OData segment is:
25
 * Segment       : identifier[(keyPredicate)]?            : e.g. Customers, Customers('ALFKI'),
26
 *                                                          Order_Details(OrderID=123, ProductID=11)
27
 * keyPredicate  : keyValue | NamedKeyValue
28
 * NamedKeyValue : keyName=keyValue [, keyName=keyValue]* : e.g. OrderID=123, ProductID=11
29
 * keyValue      : quotedValue | unquotedValue            : e.g. 'ALFKI'
30
 * quotedValue   : "'" nqChar "'"
31
 * unquotedValue : [.*]                                   : Any character
32
 * nqChar        : [^\']                                  : Character other than quotes
33
 */
34
class SegmentParser
35
{
36
    /**
37
     * The wrapper of IMetadataProvider and IQueryProvider.
38
     *
39
     * @var ProvidersWrapper
40
     */
41
    private $providerWrapper;
42
43
    /**
44
     * Array of SegmentDescriptor describing each segment in the request Uri.
45
     *
46
     * @var SegmentDescriptor[]
47
     */
48
    private $segmentDescriptors = [];
49
50
    /**
51
     * Constructs a new instance of SegmentParser.
52
     *
53
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
54
     */
55
    private function __construct(ProvidersWrapper $providerWrapper)
56
    {
57
        $this->providerWrapper = $providerWrapper;
58
    }
59
60
    /**
61
     * Parse the given Uri segments.
62
     *
63
     * @param string[]         $segments        Array of segments in the request Uri
64
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
65
     * @param bool             $checkForRights  Whether to check for rights on the resource sets in the segments
66
     *
67
     * @throws ODataException If any error occurs while processing segment
68
     *
69
     * @return SegmentDescriptor[]
70
     */
71
    public static function parseRequestUriSegments($segments, ProvidersWrapper $providerWrapper, $checkForRights = true)
72
    {
73
        $segmentParser = new self($providerWrapper);
74
        $segmentParser->createSegmentDescriptors($segments, $checkForRights);
75
76
        return $segmentParser->segmentDescriptors;
77
    }
78
79
    public function getProviderWrapper()
80
    {
81
        return $this->providerWrapper;
82
    }
83
84
    /**
85
     * Extract identifier and key predicate from a segment.
86
     *
87
     * @param string $segment       The segment from which identifier and key
88
     * @param string &$identifier   On return, this parameter will contain identifier part of the segment
89
     * @param string &$keyPredicate On return, this parameter will contain key predicate part of the segment,
90
     *                              null if predicate is absent
91
     *
92
     * @throws ODataException If any error occurs while processing segment
93
     */
94
    private function extractSegmentIdentifierAndKeyPredicate($segment, &$identifier, &$keyPredicate)
95
    {
96
        $predicateStart = strpos($segment, '(');
97
        if ($predicateStart === false) {
98
            $identifier = $segment;
99
            $keyPredicate = null;
100
101
            return;
102
        }
103
104
        $segmentLength = strlen($segment);
105
        if (strrpos($segment, ')') !== $segmentLength - 1) {
106
            throw ODataException::createSyntaxError(Messages::syntaxError());
107
        }
108
109
        $identifier = substr($segment, 0, $predicateStart);
110
        ++$predicateStart;
111
        $keyPredicate = substr($segment, $predicateStart, $segmentLength - $predicateStart - 1);
112
        $keyPredicate = str_replace('%C3%82%C2%BB', '/', $keyPredicate);
113
    }
114
115
    /**
116
     * Process a collection of OData URI segment strings and turn them into segment descriptors.
117
     *
118
     * @param string[] $segments    array of segments strings to parse
119
     * @param bool     $checkRights Whether to check for rights or not
120
     *
121
     * @throws ODataException Exception in case of any error found while precessing segments
122
     * @return mixed
123
     */
124
    private function createSegmentDescriptors($segments, $checkRights)
125
    {
126
        if (empty($segments)) {
127
            //If there's no segments, then it's the service root
128
            $descriptor = new SegmentDescriptor();
129
            $descriptor->setTargetKind(TargetKind::SERVICE_DIRECTORY());
130
            $this->segmentDescriptors[] = $descriptor;
131
132
            return;
133
        }
134
135
        $segmentCount = count($segments);
136
        $identifier = $keyPredicate = null;
137
        $this->extractSegmentIdentifierAndKeyPredicate($segments[0], $identifier, $keyPredicate);
138
        $previous = $this->createFirstSegmentDescriptor(
139
            $identifier,
140
            $keyPredicate,
141
            $checkRights
142
        );
143
        assert($previous instanceof SegmentDescriptor, get_class($previous));
144
        $this->segmentDescriptors[0] = $previous;
145
146
        for ($i = 1; $i < $segmentCount; ++$i) {
147
            $thisSegment = $segments[$i];
148
            $current = $this->createNextSegment($previous, $thisSegment, $checkRights);
149
150
            $current->setPrevious($previous);
151
            $previous->setNext($current);
152
            $this->segmentDescriptors[] = $current;
153
            $previous = $current;
154
        }
155
156
        //At this point $previous is the final segment..which cannot be a $link
157
        if ($previous->getTargetKind() == TargetKind::LINK()) {
158
            throw ODataException::createBadRequestError(Messages::segmentParserMissingSegmentAfterLink());
159
        }
160
    }
161
162
    /**
163
     * @param SegmentDescriptor $previous
164
     * @param string            $segment
165
     * @param bool              $checkRights
166
     *
167
     * @throws ODataException
168
     * @return SegmentDescriptor
169
     */
170
    private function createNextSegment(SegmentDescriptor $previous, $segment, $checkRights)
171
    {
172
        $previousKind = $previous->getTargetKind();
173
        if ($previousKind == TargetKind::METADATA()
174
            || $previousKind == TargetKind::BATCH()
175
            || $previousKind == TargetKind::PRIMITIVE_VALUE()
176
            || $previousKind == TargetKind::BAG()
177
            || $previousKind == TargetKind::MEDIA_RESOURCE()
178
        ) {
179
            //All these targets are terminal segments, there cannot be anything after them.
180
            throw ODataException::resourceNotFoundError(
181
                Messages::segmentParserMustBeLeafSegment($previous->getIdentifier())
182
            );
183
        }
184
185
        $identifier = $keyPredicate = null;
186
        $this->extractSegmentIdentifierAndKeyPredicate($segment, $identifier, $keyPredicate);
187
        $hasPredicate = null !== $keyPredicate;
188
189
        $singleton = $this->providerWrapper->resolveSingleton($identifier);
190
        if (null !== $singleton) {
191
            throw ODataException::createSyntaxError('Singleton must be first element');
192
        }
193
194
        if ($previousKind == TargetKind::PRIMITIVE()) {
195
            if ($identifier !== ODataConstants::URI_VALUE_SEGMENT) {
0 ignored issues
show
introduced by
The condition $identifier !== POData\C...ants::URI_VALUE_SEGMENT is always true.
Loading history...
196
                throw ODataException::resourceNotFoundError(
197
                    Messages::segmentParserOnlyValueSegmentAllowedAfterPrimitivePropertySegment(
198
                        $identifier,
199
                        $previous->getIdentifier()
200
                    )
201
                );
202
            }
203
204
            $this->assertion(!$hasPredicate);
205
            $current = SegmentDescriptor::createFrom($previous);
206
            $current->setIdentifier(ODataConstants::URI_VALUE_SEGMENT);
207
            $current->setTargetKind(TargetKind::PRIMITIVE_VALUE());
208
            $current->setSingleResult(true);
209
        } elseif (null !== $previous->getPrevious()
210
                  && $previous->getPrevious()->getIdentifier() === ODataConstants::URI_LINK_SEGMENT
211
                  && $identifier !== ODataConstants::URI_COUNT_SEGMENT) {
212
            throw ODataException::createBadRequestError(
213
                Messages::segmentParserNoSegmentAllowedAfterPostLinkSegment($identifier)
214
            );
215
        } elseif ($previousKind == TargetKind::RESOURCE()
216
            && $previous->isSingleResult()
217
            && $identifier === ODataConstants::URI_LINK_SEGMENT
0 ignored issues
show
introduced by
The condition $identifier === POData\C...tants::URI_LINK_SEGMENT is always false.
Loading history...
218
        ) {
219
            $this->assertion(!$hasPredicate);
220
            $current = SegmentDescriptor::createFrom($previous);
221
            $current->setIdentifier(ODataConstants::URI_LINK_SEGMENT);
222
            $current->setTargetKind(TargetKind::LINK());
223
        } else {
224
            //Do a sanity check here
225
            if ($previousKind != TargetKind::COMPLEX_OBJECT()
226
                && $previousKind != TargetKind::RESOURCE()
227
                && $previousKind != TargetKind::LINK()
228
            ) {
229
                throw ODataException::createInternalServerError(
230
                    Messages::segmentParserInconsistentTargetKindState()
231
                );
232
            }
233
234
            if (!$previous->isSingleResult() && $identifier !== ODataConstants::URI_COUNT_SEGMENT) {
235
                throw ODataException::createBadRequestError(
236
                    Messages::segmentParserCannotQueryCollection($previous->getIdentifier())
237
                );
238
            }
239
240
            $current = new SegmentDescriptor();
241
            $current->setIdentifier($identifier);
242
            $current->setTargetSource(TargetSource::PROPERTY);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Reso...\TargetSource::PROPERTY of type integer is incompatible with the type POData\UriProcessor\Reso...mentParser\TargetSource expected by parameter $targetSource of POData\UriProcessor\Reso...ptor::setTargetSource(). ( Ignorable by Annotation )

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

242
            $current->setTargetSource(/** @scrutinizer ignore-type */ TargetSource::PROPERTY);
Loading history...
243
            $previousType = $previous->getTargetResourceType();
244
            $projectedProperty = $previousType->resolveProperty($identifier);
245
            $current->setProjectedProperty($projectedProperty);
246
247
            if ($identifier === ODataConstants::URI_COUNT_SEGMENT) {
0 ignored issues
show
introduced by
The condition $identifier === POData\C...ants::URI_COUNT_SEGMENT is always false.
Loading history...
248
                if ($previousKind != TargetKind::RESOURCE()) {
249
                    throw ODataException::createBadRequestError(
250
                        Messages::segmentParserCountCannotBeApplied($previous->getIdentifier())
251
                    );
252
                }
253
254
                if ($previous->isSingleResult()) {
255
                    throw ODataException::createBadRequestError(
256
                        Messages::segmentParserCountCannotFollowSingleton($previous->getIdentifier())
257
                    );
258
                }
259
260
                $current->setTargetKind(TargetKind::PRIMITIVE_VALUE());
261
                $current->setSingleResult(true);
262
                $current->setTargetResourceSetWrapper(
263
                    $previous->getTargetResourceSetWrapper()
264
                );
265
                $current->setTargetResourceType(
266
                    $previous->getTargetResourceType()
267
                );
268
            } elseif ($identifier === ODataConstants::URI_VALUE_SEGMENT
0 ignored issues
show
introduced by
The condition $identifier === POData\C...ants::URI_VALUE_SEGMENT is always false.
Loading history...
269
                && $previousKind == TargetKind::RESOURCE()
270
            ) {
271
                $current->setSingleResult(true);
272
                $current->setTargetResourceType(
273
                    $previous->getTargetResourceType()
274
                );
275
                $current->setTargetKind(TargetKind::MEDIA_RESOURCE());
276
            } elseif (null === $projectedProperty) {
277
                if (null !== $previous->getTargetResourceType()
278
                    && null !== $previous->getTargetResourceType()->tryResolveNamedStreamByName($identifier)
279
                ) {
280
                    $current->setTargetKind(TargetKind::MEDIA_RESOURCE());
281
                    $current->setSingleResult(true);
282
                    $current->setTargetResourceType(
283
                        $previous->getTargetResourceType()
284
                    );
285
                } else {
286
                    throw ODataException::createResourceNotFoundError($identifier);
287
                }
288
            } else {
289
                $current->setTargetResourceType($projectedProperty->getResourceType());
290
                $current->setSingleResult($projectedProperty->getKind() != ResourcePropertyKind::RESOURCESET_REFERENCE);
291
                if ($previousKind == TargetKind::LINK()
292
                    && $projectedProperty->getTypeKind() != ResourceTypeKind::ENTITY()
293
                ) {
294
                    throw ODataException::createBadRequestError(
295
                        Messages::segmentParserLinkSegmentMustBeFollowedByEntitySegment(
296
                            $identifier
297
                        )
298
                    );
299
                }
300
301
                switch ($projectedProperty->getKind()) {
302
                    case ResourcePropertyKind::COMPLEX_TYPE:
303
                        $current->setTargetKind(TargetKind::COMPLEX_OBJECT());
304
                        break;
305
                    case ResourcePropertyKind::BAG | ResourcePropertyKind::PRIMITIVE:
306
                    case ResourcePropertyKind::BAG | ResourcePropertyKind::COMPLEX_TYPE:
307
                        $current->setTargetKind(TargetKind::BAG());
308
                        break;
309
                    case ResourcePropertyKind::RESOURCE_REFERENCE:
310
                    case ResourcePropertyKind::RESOURCESET_REFERENCE:
311
                        $current->setTargetKind(TargetKind::RESOURCE());
312
                        $prevResource = $previous->getTargetResourceType();
313
                        assert($prevResource instanceof ResourceEntityType);
314
                        $resourceSetWrapper = $this->providerWrapper->getResourceSetWrapperForNavigationProperty(
315
                            $previous->getTargetResourceSetWrapper(),
316
                            $prevResource,
317
                            $projectedProperty
318
                        );
319
                        if (null === $resourceSetWrapper) {
320
                            throw ODataException::createResourceNotFoundError($projectedProperty->getName());
321
                        }
322
323
                        $current->setTargetResourceSetWrapper($resourceSetWrapper);
324
                        break;
325
                    default:
326
                        if (!$projectedProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
0 ignored issues
show
Bug introduced by
POData\Providers\Metadat...PropertyKind::PRIMITIVE 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

326
                        if (!$projectedProperty->isKindOf(/** @scrutinizer ignore-type */ ResourcePropertyKind::PRIMITIVE)) {
Loading history...
327
                            throw ODataException::createInternalServerError(
328
                                Messages::segmentParserUnExpectedPropertyKind('Primitive')
329
                            );
330
                        }
331
332
                        $current->setTargetKind(TargetKind::PRIMITIVE());
333
                        break;
334
                }
335
336
                if ($hasPredicate) {
0 ignored issues
show
introduced by
The condition $hasPredicate is always false.
Loading history...
337
                    $this->assertion(!$current->isSingleResult());
338
                    $keyDescriptor = $this->createKeyDescriptor(
339
                        $identifier . '(' . $keyPredicate . ')',
340
                        $projectedProperty->getResourceType(),
341
                        $keyPredicate
342
                    );
343
                    $current->setKeyDescriptor($keyDescriptor);
344
                    if (!$keyDescriptor->isEmpty()) {
345
                        $current->setSingleResult(true);
346
                    }
347
                }
348
349
                if ($checkRights && null !== $current->getTargetResourceSetWrapper()) {
350
                    $current->getTargetResourceSetWrapper()
351
                        ->checkResourceSetRightsForRead(
352
                            $current->isSingleResult()
353
                        );
354
                }
355
            }
356
        }
357
358
        return $current;
359
    }
360
361
    /**
362
     * Create SegmentDescriptor for the first segment.
363
     *
364
     * @param string $segmentIdentifier The identifier part of the first segment
365
     * @param string $keyPredicate      The predicate part of the first segment if any else NULL
366
     * @param bool   $checkRights       Whether to check the rights on this segment
367
     *
368
     * @throws ODataException Exception if any validation fails
369
     *
370
     * @return SegmentDescriptor Descriptor for the first segment
371
     */
372
    private function createFirstSegmentDescriptor($segmentIdentifier, $keyPredicate, $checkRights)
373
    {
374
        $descriptor = new SegmentDescriptor();
375
        $descriptor->setIdentifier($segmentIdentifier);
376
377
        if ($segmentIdentifier === ODataConstants::URI_METADATA_SEGMENT) {
378
            $this->assertion(null === $keyPredicate);
379
            $descriptor->setTargetKind(TargetKind::METADATA());
380
381
            return $descriptor;
382
        }
383
384
        if ($segmentIdentifier === ODataConstants::URI_BATCH_SEGMENT) {
385
            $this->assertion(null === $keyPredicate);
386
            $descriptor->setTargetKind(TargetKind::BATCH());
387
388
            return $descriptor;
389
        }
390
391
        if ($segmentIdentifier === ODataConstants::URI_COUNT_SEGMENT) {
392
            throw ODataException::createBadRequestError(
393
                Messages::segmentParserSegmentNotAllowedOnRoot(
394
                    ODataConstants::URI_COUNT_SEGMENT
395
                )
396
            );
397
        }
398
399
        if ($segmentIdentifier === ODataConstants::URI_LINK_SEGMENT) {
400
            throw ODataException::createBadRequestError(
401
                Messages::segmentParserSegmentNotAllowedOnRoot(
402
                    ODataConstants::URI_LINK_SEGMENT
403
                )
404
            );
405
        }
406
407
        $singleton = $this->getProviderWrapper()->resolveSingleton($segmentIdentifier);
408
        if (null !== $singleton) {
409
            $this->assertion(null === $keyPredicate);
410
            $resourceType = $singleton->getResourceType();
411
            $resourceSet = $resourceType->getCustomState();
412
            assert($resourceSet instanceof ResourceSet, get_class($resourceSet));
413
            $typeName = $resourceSet->getName();
414
            $resourceSet = $this->providerWrapper->resolveResourceSet($typeName);
415
            assert($resourceSet instanceof ResourceSetWrapper);
416
            $descriptor->setTargetKind(TargetKind::SINGLETON());
417
            $descriptor->setTargetSource(TargetSource::ENTITY_SET);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Reso...argetSource::ENTITY_SET of type integer is incompatible with the type POData\UriProcessor\Reso...mentParser\TargetSource expected by parameter $targetSource of POData\UriProcessor\Reso...ptor::setTargetSource(). ( Ignorable by Annotation )

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

417
            $descriptor->setTargetSource(/** @scrutinizer ignore-type */ TargetSource::ENTITY_SET);
Loading history...
418
            $descriptor->setTargetResourceType($resourceType);
419
            $descriptor->setTargetResourceSetWrapper($resourceSet);
420
            $descriptor->setSingleResult(true);
421
422
            return $descriptor;
423
        }
424
425
        $resourceSetWrapper = $this->getProviderWrapper()->resolveResourceSet($segmentIdentifier);
426
        if (null === $resourceSetWrapper) {
427
            throw ODataException::createResourceNotFoundError($segmentIdentifier);
428
        }
429
430
        $descriptor->setTargetResourceSetWrapper($resourceSetWrapper);
431
        $descriptor->setTargetResourceType($resourceSetWrapper->getResourceType());
432
        $descriptor->setTargetSource(TargetSource::ENTITY_SET);
433
        $descriptor->setTargetKind(TargetKind::RESOURCE());
434
        if (null !== $keyPredicate) {
0 ignored issues
show
introduced by
The condition null !== $keyPredicate is always true.
Loading history...
435
            $keyDescriptor = $this->createKeyDescriptor(
436
                $segmentIdentifier . '(' . $keyPredicate . ')',
437
                $resourceSetWrapper->getResourceType(),
438
                $keyPredicate
439
            );
440
            $descriptor->setKeyDescriptor($keyDescriptor);
441
            if (!$keyDescriptor->isEmpty()) {
442
                $descriptor->setSingleResult(true);
443
            }
444
        }
445
446
        if ($checkRights) {
447
            $resourceSetWrapper->checkResourceSetRightsForRead(
448
                $descriptor->isSingleResult()
449
            );
450
        }
451
        return $descriptor;
452
    }
453
454
    /**
455
     * Creates an instance of KeyDescriptor by parsing a key predicate, also
456
     * validates the KeyDescriptor.
457
     *
458
     * @param string       $segment      The uri segment in the form identifier
459
     *                                   (keyPredicate)
460
     * @param ResourceType $resourceType The Resource type whose keys need to
461
     *                                   be parsed
462
     * @param string       $keyPredicate The key predicate to parse and generate
463
     *                                   KeyDescriptor for
464
     *
465
     * @throws ODataException Exception if any error occurs while parsing and
466
     *                        validating the key predicate
467
     *
468
     * @return KeyDescriptor|null Describes the key values in the $keyPredicate
469
     */
470
    private function createKeyDescriptor($segment, ResourceType $resourceType, $keyPredicate)
471
    {
472
        /**
473
         * @var KeyDescriptor
474
         */
475
        $keyDescriptor = null;
476
        if (!KeyDescriptor::tryParseKeysFromKeyPredicate($keyPredicate, $keyDescriptor)) {
477
            throw ODataException::createSyntaxError(Messages::syntaxError());
478
        }
479
480
        // Note: Currently WCF Data Service does not support multiple
481
        // 'Positional values' so Order_Details(10248, 11) is not valid
482
        if (!$keyDescriptor->isEmpty()
483
            && !$keyDescriptor->areNamedValues()
484
            && $keyDescriptor->valueCount() > 1
485
        ) {
486
            throw ODataException::createSyntaxError(
487
                Messages::segmentParserKeysMustBeNamed($segment)
488
            );
489
        }
490
        $keyDescriptor->validate($segment, $resourceType);
491
492
        return $keyDescriptor;
493
    }
494
495
    /**
496
     * Assert that the given condition is true, if false throw
497
     * ODataException for syntax error.
498
     *
499
     * @param bool $condition The condition to assert
500
     *
501
     * @throws ODataException
502
     */
503
    private function assertion($condition)
504
    {
505
        if (!$condition) {
506
            throw ODataException::createSyntaxError(Messages::syntaxError());
507
        }
508
    }
509
}
510