SegmentParser   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 490
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 234
c 8
b 0
f 0
dl 0
loc 490
rs 3.28
wmc 64

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A createKeyDescriptor() 0 26 5
A getProviderWrapper() 0 3 1
B createFirstSegmentDescriptor() 0 84 10
A createSegmentDescriptors() 0 35 4
A extractSegmentIdentifierAndKeyPredicate() 0 21 3
A assertion() 0 4 2
F createNextSegment() 0 188 37
A parseRequestUriSegments() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like SegmentParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SegmentParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\UriProcessor\ResourcePathProcessor\SegmentParser;
6
7
use POData\Common\Messages;
8
use POData\Common\ODataConstants;
9
use POData\Common\ODataException;
10
use POData\Providers\Metadata\ResourceEntityType;
11
use POData\Providers\Metadata\ResourcePropertyKind;
12
use POData\Providers\Metadata\ResourceSet;
13
use POData\Providers\Metadata\ResourceSetWrapper;
14
use POData\Providers\Metadata\ResourceType;
15
use POData\Providers\Metadata\ResourceTypeKind;
16
use POData\Providers\ProvidersWrapper;
17
use ReflectionException;
18
19
/**
20
 * Class SegmentParser.
21
 *
22
 * A parser to parse the segments in OData URI, Uri is made up of bunch of segments,
23
 * each segment is separated by '/' character
24
 * e.g. Customers('ALFKI')/Orders(2134)/Order_Details/Product
25
 *
26
 * Syntax of an OData segment is:
27
 * Segment       : identifier[(keyPredicate)]?            : e.g. Customers, Customers('ALFKI'),
28
 *                                                          Order_Details(OrderID=123, ProductID=11)
29
 * keyPredicate  : keyValue | NamedKeyValue
30
 * NamedKeyValue : keyName=keyValue [, keyName=keyValue]* : e.g. OrderID=123, ProductID=11
31
 * keyValue      : quotedValue | unquotedValue            : e.g. 'ALFKI'
32
 * quotedValue   : "'" nqChar "'"
33
 * unquotedValue : [.*]                                   : Any character
34
 * nqChar        : [^\']                                  : Character other than quotes
35
 */
36
class SegmentParser
37
{
38
    /**
39
     * The wrapper of IMetadataProvider and IQueryProvider.
40
     *
41
     * @var ProvidersWrapper
42
     */
43
    private $providerWrapper;
44
45
    /**
46
     * Array of SegmentDescriptor describing each segment in the request Uri.
47
     *
48
     * @var SegmentDescriptor[]
49
     */
50
    private $segmentDescriptors = [];
51
52
    /**
53
     * Constructs a new instance of SegmentParser.
54
     *
55
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
56
     */
57
    private function __construct(ProvidersWrapper $providerWrapper)
58
    {
59
        $this->providerWrapper = $providerWrapper;
60
    }
61
62
    /**
63
     * Parse the given Uri segments.
64
     *
65
     * @param string[]         $segments        Array of segments in the request Uri
66
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
67
     * @param bool             $checkForRights  Whether to check for rights on the resource sets in the segments
68
     *
69
     * @throws ReflectionException
70
     * @throws ODataException      If any error occurs while processing segment
71
     * @return SegmentDescriptor[]
72
     */
73
    public static function parseRequestUriSegments(
74
        array $segments,
75
        ProvidersWrapper $providerWrapper,
76
        bool $checkForRights = true
77
    ): array {
78
        $segmentParser = new self($providerWrapper);
79
        $segmentParser->createSegmentDescriptors($segments, $checkForRights);
80
81
        return $segmentParser->segmentDescriptors;
82
    }
83
84
    /**
85
     * Process a collection of OData URI segment strings and turn them into segment descriptors.
86
     *
87
     * @param string[] $segments    array of segments strings to parse
88
     * @param bool     $checkRights Whether to check for rights or not
89
     *
90
     * @throws ReflectionException
91
     * @throws ODataException      Exception in case of any error found while precessing segments
92
     * @return mixed
93
     */
94
    private function createSegmentDescriptors(array $segments, bool $checkRights): void
95
    {
96
        if (empty($segments)) {
97
            //If there's no segments, then it's the service root
98
            $descriptor = new SegmentDescriptor();
99
            $descriptor->setTargetKind(TargetKind::SERVICE_DIRECTORY());
100
            $this->segmentDescriptors[] = $descriptor;
101
102
            return;
103
        }
104
105
        $segmentCount = count($segments);
106
        $keyPredicate = null;
107
        $identifier   = $this->extractSegmentIdentifierAndKeyPredicate($segments[0], $keyPredicate);
108
        $previous     = $this->createFirstSegmentDescriptor(
109
            $identifier,
110
            $keyPredicate,
111
            $checkRights
112
        );
113
        assert($previous instanceof SegmentDescriptor, get_class($previous));
114
        $this->segmentDescriptors[0] = $previous;
115
116
        for ($i = 1; $i < $segmentCount; ++$i) {
117
            $thisSegment = $segments[$i];
118
            $current     = $this->createNextSegment($previous, $thisSegment, $checkRights);
119
120
            $current->setPrevious($previous);
121
            $previous->setNext($current);
122
            $this->segmentDescriptors[] = $current;
123
            $previous                   = $current;
124
        }
125
126
        //At this point $previous is the final segment..which cannot be a $link
127
        if ($previous->getTargetKind() == TargetKind::LINK()) {
128
            throw ODataException::createBadRequestError(Messages::segmentParserMissingSegmentAfterLink());
129
        }
130
    }
131
132
    /**
133
     * Extract identifier and key predicate from a segment.
134
     *
135
     * @param string $segment      The segment from which identifier and key
136
     * @param string $keyPredicate On return, this parameter will contain key predicate part of the segment,
137
     *                             null if predicate is absent
138
     *
139
     * @throws ODataException If any error occurs while processing segment
140
     * @return string         The identifier part of the segment
141
     */
142
    private function extractSegmentIdentifierAndKeyPredicate(string $segment, ?string &$keyPredicate): string
143
    {
144
        $predicateStart = strpos($segment, '(');
145
        if ($predicateStart === false) {
146
            $identifier   = $segment;
147
            $keyPredicate = null;
148
149
            return $identifier;
150
        }
151
152
        $segmentLength = strlen($segment);
153
        if (strrpos($segment, ')') !== $segmentLength - 1) {
154
            throw ODataException::createSyntaxError(Messages::syntaxError());
155
        }
156
157
        $identifier = substr($segment, 0, $predicateStart);
158
        ++$predicateStart;
159
        $keyPredicate = substr($segment, $predicateStart, $segmentLength - $predicateStart - 1);
160
        $keyPredicate = str_replace('%C3%82%C2%BB', '/', $keyPredicate);
161
162
        return $identifier;
163
    }
164
165
    /**
166
     * Create SegmentDescriptor for the first segment.
167
     *
168
     * @param string $segmentIdentifier The identifier part of the first segment
169
     * @param string $keyPredicate      The predicate part of the first segment if any else NULL
170
     * @param bool   $checkRights       Whether to check the rights on this segment
171
     *
172
     * @throws ReflectionException
173
     * @throws ODataException      Exception if any validation fails
174
     * @return SegmentDescriptor   Descriptor for the first segment
175
     */
176
    private function createFirstSegmentDescriptor(
177
        string $segmentIdentifier,
178
        ?string $keyPredicate,
179
        bool $checkRights
180
    ): SegmentDescriptor {
181
        $descriptor = new SegmentDescriptor();
182
        $descriptor->setIdentifier($segmentIdentifier);
183
184
        if ($segmentIdentifier === ODataConstants::URI_METADATA_SEGMENT) {
185
            $this->assertion(null === $keyPredicate);
186
            $descriptor->setTargetKind(TargetKind::METADATA());
187
188
            return $descriptor;
189
        }
190
191
        if ($segmentIdentifier === ODataConstants::URI_BATCH_SEGMENT) {
192
            $this->assertion(null === $keyPredicate);
193
            $descriptor->setTargetKind(TargetKind::BATCH());
194
195
            return $descriptor;
196
        }
197
198
        if ($segmentIdentifier === ODataConstants::URI_COUNT_SEGMENT) {
199
            throw ODataException::createBadRequestError(
200
                Messages::segmentParserSegmentNotAllowedOnRoot(
201
                    ODataConstants::URI_COUNT_SEGMENT
202
                )
203
            );
204
        }
205
206
        if ($segmentIdentifier === ODataConstants::URI_LINK_SEGMENT) {
207
            throw ODataException::createBadRequestError(
208
                Messages::segmentParserSegmentNotAllowedOnRoot(
209
                    ODataConstants::URI_LINK_SEGMENT
210
                )
211
            );
212
        }
213
214
        $singleton = $this->getProviderWrapper()->resolveSingleton($segmentIdentifier);
215
        if (null !== $singleton) {
216
            $this->assertion(null === $keyPredicate);
217
            /** @var ResourceType $resourceType */
218
            $resourceType = $singleton->getResourceType();
219
            $resourceSet  = $resourceType->getCustomState();
220
            assert($resourceSet instanceof ResourceSet, get_class($resourceSet));
221
            $typeName    = $resourceSet->getName();
222
            $resourceSet = $this->providerWrapper->resolveResourceSet($typeName);
223
            assert($resourceSet instanceof ResourceSetWrapper);
224
            $descriptor->setTargetKind(TargetKind::SINGLETON());
225
            $descriptor->setTargetSource(TargetSource::ENTITY_SET());
226
            $descriptor->setTargetResourceType($resourceType);
227
            $descriptor->setTargetResourceSetWrapper($resourceSet);
228
            $descriptor->setSingleResult(true);
229
230
            return $descriptor;
231
        }
232
233
        $resourceSetWrapper = $this->getProviderWrapper()->resolveResourceSet($segmentIdentifier);
234
        if (null === $resourceSetWrapper) {
235
            throw ODataException::createResourceNotFoundError($segmentIdentifier);
236
        }
237
238
        $descriptor->setTargetResourceSetWrapper($resourceSetWrapper);
239
        $descriptor->setTargetResourceType($resourceSetWrapper->getResourceType());
240
        $descriptor->setTargetSource(TargetSource::ENTITY_SET());
241
        $descriptor->setTargetKind(TargetKind::RESOURCE());
242
        if (null !== $keyPredicate) {
243
            $keyDescriptor = $this->createKeyDescriptor(
244
                $segmentIdentifier . '(' . $keyPredicate . ')',
245
                $resourceSetWrapper->getResourceType(),
246
                $keyPredicate
247
            );
248
            $descriptor->setKeyDescriptor($keyDescriptor);
249
            if (!$keyDescriptor->isEmpty()) {
250
                $descriptor->setSingleResult(true);
251
            }
252
        }
253
254
        if ($checkRights) {
255
            $resourceSetWrapper->checkResourceSetRightsForRead(
256
                $descriptor->isSingleResult()
257
            );
258
        }
259
        return $descriptor;
260
    }
261
262
    /**
263
     * Assert that the given condition is true, if false throw
264
     * ODataException for syntax error.
265
     *
266
     * @param bool $condition The condition to assert
267
     *
268
     * @throws ODataException
269
     */
270
    private function assertion(bool $condition): void
271
    {
272
        if (!$condition) {
273
            throw ODataException::createSyntaxError(Messages::syntaxError());
274
        }
275
    }
276
277
    /**
278
     * @return ProvidersWrapper
279
     */
280
    public function getProviderWrapper(): ProvidersWrapper
281
    {
282
        return $this->providerWrapper;
283
    }
284
285
    /**
286
     * Creates an instance of KeyDescriptor by parsing a key predicate, also
287
     * validates the KeyDescriptor.
288
     *
289
     * @param string       $segment      The uri segment in the form identifier
290
     *                                   (keyPredicate)
291
     * @param ResourceType $resourceType The Resource type whose keys need to
292
     *                                   be parsed
293
     * @param string       $keyPredicate The key predicate to parse and generate
294
     *                                   KeyDescriptor for
295
     *
296
     * @throws ReflectionException
297
     * @throws ODataException      Exception if any error occurs while parsing and
298
     *                             validating the key predicate
299
     * @return KeyDescriptor|null  Describes the key values in the $keyPredicate
300
     */
301
    private function createKeyDescriptor(
302
        string $segment,
303
        ResourceType $resourceType,
304
        string $keyPredicate
305
    ): ?KeyDescriptor {
306
        /**
307
         * @var KeyDescriptor|null $keyDescriptor
308
         */
309
        $keyDescriptor = null;
310
        if (!KeyDescriptor::tryParseKeysFromKeyPredicate($keyPredicate, $keyDescriptor)) {
311
            throw ODataException::createSyntaxError(Messages::syntaxError());
312
        }
313
314
        // Note: Currently WCF Data Service does not support multiple
315
        // 'Positional values' so Order_Details(10248, 11) is not valid
316
        if (!$keyDescriptor->isEmpty()
317
            && !$keyDescriptor->areNamedValues()
318
            && $keyDescriptor->valueCount() > 1
319
        ) {
320
            throw ODataException::createSyntaxError(
321
                Messages::segmentParserKeysMustBeNamed($segment)
322
            );
323
        }
324
        $keyDescriptor->validate($segment, $resourceType);
325
326
        return $keyDescriptor;
327
    }
328
329
    /**
330
     * @param SegmentDescriptor $previous
331
     * @param string            $segment
332
     * @param bool              $checkRights
333
     *
334
     * @throws ReflectionException
335
     * @throws ODataException
336
     * @return SegmentDescriptor
337
     */
338
    private function createNextSegment(
339
        SegmentDescriptor $previous,
340
        string $segment,
341
        bool $checkRights
342
    ): SegmentDescriptor {
343
        $previousKind = $previous->getTargetKind();
344
        if ($previousKind->isTerminal()) {
345
            //All these targets are terminal segments, there cannot be anything after them.
346
            throw ODataException::resourceNotFoundError(
347
                Messages::segmentParserMustBeLeafSegment($previous->getIdentifier())
348
            );
349
        }
350
351
        $keyPredicate = null;
352
        $identifier   = $this->extractSegmentIdentifierAndKeyPredicate($segment, $keyPredicate);
353
        $hasPredicate = null !== $keyPredicate;
354
355
        $singleton = $this->providerWrapper->resolveSingleton($identifier);
356
        if (null !== $singleton) {
357
            throw ODataException::createSyntaxError('Singleton must be first element');
358
        }
359
360
        if ($previousKind == TargetKind::PRIMITIVE()) {
361
            if ($identifier !== ODataConstants::URI_VALUE_SEGMENT) {
362
                throw ODataException::resourceNotFoundError(
363
                    Messages::segmentParserOnlyValueSegmentAllowedAfterPrimitivePropertySegment(
364
                        $identifier,
365
                        $previous->getIdentifier()
366
                    )
367
                );
368
            }
369
370
            $this->assertion(!$hasPredicate);
371
            $current = SegmentDescriptor::createFrom($previous);
372
            $current->setIdentifier(ODataConstants::URI_VALUE_SEGMENT);
373
            $current->setTargetKind(TargetKind::PRIMITIVE_VALUE());
374
            $current->setSingleResult(true);
375
        } elseif (null !== $previous->getPrevious()
376
            && $previous->getPrevious()->getIdentifier() === ODataConstants::URI_LINK_SEGMENT
377
            && $identifier !== ODataConstants::URI_COUNT_SEGMENT) {
378
            throw ODataException::createBadRequestError(
379
                Messages::segmentParserNoSegmentAllowedAfterPostLinkSegment($identifier)
380
            );
381
        } elseif ($previousKind == TargetKind::RESOURCE()
382
            && $previous->isSingleResult()
383
            && $identifier === ODataConstants::URI_LINK_SEGMENT
384
        ) {
385
            $this->assertion(!$hasPredicate);
386
            $current = SegmentDescriptor::createFrom($previous);
387
            $current->setIdentifier(ODataConstants::URI_LINK_SEGMENT);
388
            $current->setTargetKind(TargetKind::LINK());
389
        } else {
390
            //Do a sanity check here
391
            if ($previousKind != TargetKind::COMPLEX_OBJECT()
392
                && $previousKind != TargetKind::RESOURCE()
393
                && $previousKind != TargetKind::LINK()
394
            ) {
395
                throw ODataException::createInternalServerError(
396
                    Messages::segmentParserInconsistentTargetKindState()
397
                );
398
            }
399
400
            if (!$previous->isSingleResult() && $identifier !== ODataConstants::URI_COUNT_SEGMENT) {
401
                throw ODataException::createBadRequestError(
402
                    Messages::segmentParserCannotQueryCollection($previous->getIdentifier())
403
                );
404
            }
405
406
            $current = new SegmentDescriptor();
407
            $current->setIdentifier($identifier);
408
            $current->setTargetSource(TargetSource::PROPERTY());
409
            $previousType      = $previous->getTargetResourceType();
410
            $projectedProperty = $previousType->resolveProperty($identifier);
411
            $current->setProjectedProperty($projectedProperty);
412
413
            if ($identifier === ODataConstants::URI_COUNT_SEGMENT) {
414
                if ($previousKind != TargetKind::RESOURCE()) {
415
                    throw ODataException::createBadRequestError(
416
                        Messages::segmentParserCountCannotBeApplied($previous->getIdentifier())
417
                    );
418
                }
419
420
                if ($previous->isSingleResult()) {
421
                    throw ODataException::createBadRequestError(
422
                        Messages::segmentParserCountCannotFollowSingleton($previous->getIdentifier())
423
                    );
424
                }
425
426
                $current->setTargetKind(TargetKind::PRIMITIVE_VALUE());
427
                $current->setSingleResult(true);
428
                $current->setTargetResourceSetWrapper(
429
                    $previous->getTargetResourceSetWrapper()
430
                );
431
                $current->setTargetResourceType(
432
                    $previous->getTargetResourceType()
433
                );
434
            } elseif ($identifier === ODataConstants::URI_VALUE_SEGMENT
435
                && $previousKind == TargetKind::RESOURCE()
436
            ) {
437
                $current->setSingleResult(true);
438
                $current->setTargetResourceType(
439
                    $previous->getTargetResourceType()
440
                );
441
                $current->setTargetKind(TargetKind::MEDIA_RESOURCE());
442
            } elseif (null === $projectedProperty) {
443
                if (null !== $previous->getTargetResourceType()
444
                    && null !== $previous->getTargetResourceType()->tryResolveNamedStreamByName($identifier)
445
                ) {
446
                    $current->setTargetKind(TargetKind::MEDIA_RESOURCE());
447
                    $current->setSingleResult(true);
448
                    $current->setTargetResourceType(
449
                        $previous->getTargetResourceType()
450
                    );
451
                } else {
452
                    throw ODataException::createResourceNotFoundError($identifier);
453
                }
454
            } else {
455
                $current->setTargetResourceType($projectedProperty->getResourceType());
456
                $rawKind = $projectedProperty->getKind();
457
                $current->setSingleResult($rawKind != ResourcePropertyKind::RESOURCESET_REFERENCE());
458
                if ($previousKind == TargetKind::LINK()
459
                    && $projectedProperty->getTypeKind() != ResourceTypeKind::ENTITY()
460
                ) {
461
                    throw ODataException::createBadRequestError(
462
                        Messages::segmentParserLinkSegmentMustBeFollowedByEntitySegment(
463
                            $identifier
464
                        )
465
                    );
466
                }
467
468
                switch ($rawKind) {
469
                    case ResourcePropertyKind::COMPLEX_TYPE():
470
                        $current->setTargetKind(TargetKind::COMPLEX_OBJECT());
471
                        break;
472
                    case ResourcePropertyKind::BAG()->setPRIMITIVE(true):
473
                    case ResourcePropertyKind::BAG()->setCOMPLEX_TYPE(true):
474
                        $current->setTargetKind(TargetKind::BAG());
475
                        break;
476
                    case ResourcePropertyKind::RESOURCE_REFERENCE():
477
                    case ResourcePropertyKind::RESOURCESET_REFERENCE():
478
                        $current->setTargetKind(TargetKind::RESOURCE());
479
                        $prevResource = $previous->getTargetResourceType();
480
                        $this->assertion($prevResource instanceof ResourceEntityType);
481
                        $resourceSetWrapper = $this->providerWrapper->getResourceSetWrapperForNavigationProperty(
482
                            $previous->getTargetResourceSetWrapper(),
483
                            $prevResource,
484
                            $projectedProperty
485
                        );
486
                        if (null === $resourceSetWrapper) {
487
                            throw ODataException::createResourceNotFoundError($projectedProperty->getName());
488
                        }
489
490
                        $current->setTargetResourceSetWrapper($resourceSetWrapper);
491
                        break;
492
                    default:
493
                        if (!$projectedProperty->isKindOf(ResourcePropertyKind::PRIMITIVE())) {
494
                            throw ODataException::createInternalServerError(
495
                                Messages::segmentParserUnExpectedPropertyKind('Primitive')
496
                            );
497
                        }
498
499
                        $current->setTargetKind(TargetKind::PRIMITIVE());
500
                        break;
501
                }
502
503
                if ($hasPredicate) {
0 ignored issues
show
introduced by
The condition $hasPredicate is always false.
Loading history...
504
                    $this->assertion(!$current->isSingleResult());
505
                    $keyDescriptor = $this->createKeyDescriptor(
506
                        $identifier . '(' . $keyPredicate . ')',
507
                        $projectedProperty->getResourceType(),
508
                        $keyPredicate
509
                    );
510
                    $current->setKeyDescriptor($keyDescriptor);
511
                    if (!$keyDescriptor->isEmpty()) {
512
                        $current->setSingleResult(true);
513
                    }
514
                }
515
516
                if ($checkRights && null !== $current->getTargetResourceSetWrapper()) {
517
                    $current->getTargetResourceSetWrapper()
518
                        ->checkResourceSetRightsForRead(
519
                            $current->isSingleResult()
520
                        );
521
                }
522
            }
523
        }
524
525
        return $current;
526
    }
527
}
528