SegmentParser::_createKeyDescriptor()   B
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.439
c 0
b 0
f 0
cc 5
eloc 11
nc 3
nop 3
1
<?php
2
3
namespace POData\UriProcessor\ResourcePathProcessor\SegmentParser;
4
5
use POData\Providers\Metadata\ResourceType;
6
use POData\Providers\Metadata\ResourceTypeKind;
7
use POData\Providers\Metadata\ResourcePropertyKind;
8
use POData\Providers\ProvidersWrapper;
9
use POData\Common\ODataConstants;
10
use POData\Common\Messages;
11
use POData\Common\ODataException;
12
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\SegmentDescriptor;
13
14
/**
15
 * Class SegmentParser
16
 *
17
 * A parser to parse the segments in OData URI, Uri is made up of bunch of segments,
18
 * each segment is separated by '/' character
19
 * e.g. Customers('ALFKI')/Orders(2134)/Order_Details/Product
20
 *
21
 * Syntax of an OData segment is:
22
 * Segment       : identifier[(keyPredicate)]?            : e.g. Customers, Customers('ALFKI'), Order_Details(OrderID=123, ProductID=11)
23
 * keyPredicate  : keyValue | NamedKeyValue
24
 * NamedKeyValue : keyName=keyValue [, keyName=keyValue]* : e.g. OrderID=123, ProductID=11
25
 * keyValue      : quotedValue | unquotedValue            : e.g. 'ALFKI'
26
 * quotedValue   : "'" nqChar "'"
27
 * unquotedValue : [.*]                                   : Any character
28
 * nqChar        : [^\']                                  : Character other than quotes
29
 *
30
 * @package POData\UriProcessor\ResourcePathProcessor\SegmentParser
31
 */
32
class SegmentParser
33
{
34
    /**
35
     * The wrapper of IMetadataProvider and IQueryProvider
36
     *
37
     * @var ProvidersWrapper
38
     */
39
    private $providerWrapper;
40
41
    /**
42
     * Array of SegmentDescriptor describing each segment in the request Uri
43
     * 
44
     * @var SegmentDescriptor[]
45
     */
46
    private $_segmentDescriptors = array();
47
48
    /**
49
     * Constructs a new instance of SegmentParser
50
     * 
51
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
52
     *
53
     */
54
    private function __construct(ProvidersWrapper $providerWrapper ) {
55
        $this->providerWrapper = $providerWrapper;
56
    }
57
58
    /**
59
     * Parse the given Uri segments
60
     * 
61
     * @param string[] $segments Array of segments in the request Uri
62
     *
63
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
64
     * @param boolean $checkForRights  Whether to check for rights on the resource sets in the segments
65
     *
66
     * @return SegmentDescriptor[]
67
     * 
68
     * @throws ODataException If any error occurs while processing segment
69
     */
70
    public static function parseRequestUriSegments($segments, ProvidersWrapper $providerWrapper, $checkForRights = true) {
71
        $segmentParser = new SegmentParser($providerWrapper);
72
        $segmentParser->createSegmentDescriptors($segments, $checkForRights);
73
        return $segmentParser->_segmentDescriptors;
74
    }
75
76
    /**
77
     * Extract identifier and key predicate from a segment
78
     * 
79
     * @param string $segment The segment from which identifier and key
80
     * @param string &$identifier   On return, this parameter will contain identifier part of the segment
81
     * @param string &$keyPredicate On return, this parameter will contain key predicate part of the segment, null if predicate is absent
82
     *
83
     * @throws ODataException If any error occurs while processing segment
84
     */
85
    private function extractSegmentIdentifierAndKeyPredicate($segment, &$identifier, &$keyPredicate) {
86
        $predicateStart = strpos($segment, '(');
87
        if ($predicateStart === false) {
88
            $identifier = $segment;
89
            $keyPredicate = null;
90
            return;
91
        }
92
93
        $segmentLength = strlen($segment);
94
        if (strrpos($segment, ')') !== $segmentLength - 1) {
95
			throw ODataException::createSyntaxError(Messages::syntaxError());
96
        }
97
98
        $identifier = substr($segment, 0, $predicateStart);
99
        $predicateStart++;
100
        $keyPredicate = substr($segment, $predicateStart, $segmentLength - $predicateStart - 1);
101
    }
102
103
    /**
104
     * Process a collection of OData URI segment strings and turn them into segment descriptors
105
     * 
106
     * @param string[] $segments array of segments strings to parse
107
     * @param boolean $checkRights Whether to check for rights or not
108
     * 
109
     * @throws ODataException Exception in case of any error found while precessing segments
110
     */
111
    private function createSegmentDescriptors($segments, $checkRights)
112
    {        
113
        if (empty($segments)) {
114
            //If there's no segments, then it's the service root
115
	        $descriptor = new SegmentDescriptor();
116
	        $descriptor->setTargetKind(TargetKind::SERVICE_DIRECTORY());
117
            $this->_segmentDescriptors[] = $descriptor;
118
            return;
119
        }
120
121
        $segmentCount = count($segments);
122
        $identifier = $keyPredicate = null;
123
        $this->extractSegmentIdentifierAndKeyPredicate($segments[0], $identifier, $keyPredicate);
124
	    $previous = $this->_createFirstSegmentDescriptor(
125
            $identifier, $keyPredicate, $checkRights
126
        );
127
        $this->_segmentDescriptors[0] = $previous;
128
129
        for ($i = 1; $i < $segmentCount; $i++) {
130
	        $current = $this->createNextSegment($previous, $segments[$i], $checkRights);
131
132
            
133
            $current->setPrevious($previous);
134
            $previous->setNext($current);
135
            $this->_segmentDescriptors[] = $current;
136
            $previous = $current;
137
        }
138
139
        //At this point $previous is the final segment..which cannot be a $link
140
        if ($previous->getTargetKind() == TargetKind::LINK()) {
141
			throw ODataException::createBadRequestError(Messages::segmentParserMissingSegmentAfterLink());
142
        }
143
    }
144
145
    private function createNextSegment(SegmentDescriptor $previous, $segment, $checkRights){
146
        $previousKind = $previous->getTargetKind();
147
        if ($previousKind == TargetKind::METADATA()
148
            || $previousKind == TargetKind::BATCH()
149
            || $previousKind == TargetKind::PRIMITIVE_VALUE()
150
            || $previousKind == TargetKind::BAG()
151
            || $previousKind == TargetKind::MEDIA_RESOURCE()
152
        ) {
153
            //All these targets are terminal segments, there cannot be anything after them.
154
			throw ODataException::resourceNotFoundError(
155
                Messages::segmentParserMustBeLeafSegment($previous->getIdentifier())
156
            );
157
        }
158
159
        $identifier = $keyPredicate = null;
160
        $this->extractSegmentIdentifierAndKeyPredicate($segment, $identifier, $keyPredicate);
161
        $hasPredicate = !is_null($keyPredicate);
162
        $current = null;
0 ignored issues
show
Unused Code introduced by
$current is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
163
        if ($previousKind == TargetKind::PRIMITIVE()) {
164
            if ($identifier !== ODataConstants::URI_VALUE_SEGMENT) {
165
				throw ODataException::resourceNotFoundError(
166
                    Messages::segmentParserOnlyValueSegmentAllowedAfterPrimitivePropertySegment(
167
                        $identifier, $previous->getIdentifier()
168
                    )
169
                );
170
            }
171
172
            $this->_assertion(!$hasPredicate);
173
            $current = SegmentDescriptor::createFrom($previous);
174
            $current->setIdentifier(ODataConstants::URI_VALUE_SEGMENT);
175
            $current->setTargetKind(TargetKind::PRIMITIVE_VALUE());
176
            $current->setSingleResult(true);
177
        } else if (!is_null($previous->getPrevious()) && $previous->getPrevious()->getIdentifier() === ODataConstants::URI_LINK_SEGMENT && $identifier !== ODataConstants::URI_COUNT_SEGMENT) {
178
			throw ODataException::createBadRequestError(
179
                Messages::segmentParserNoSegmentAllowedAfterPostLinkSegment($identifier)
180
            );
181
        } else if ($previousKind == TargetKind::RESOURCE()
182
            && $previous->isSingleResult()
183
            && $identifier === ODataConstants::URI_LINK_SEGMENT
184
        ) {
185
            $this->_assertion(!$hasPredicate);
186
            $current = SegmentDescriptor::createFrom($previous);
187
            $current->setIdentifier(ODataConstants::URI_LINK_SEGMENT);
188
            $current->setTargetKind(TargetKind::LINK());
189
        } else {
190
            //Do a sanity check here
191
            if ($previousKind != TargetKind::COMPLEX_OBJECT()
192
                && $previousKind != TargetKind::RESOURCE()
193
                && $previousKind != TargetKind::LINK()
194
            ) {
195
                throw ODataException::createInternalServerError(
196
                    Messages::segmentParserInconsistentTargetKindState()
197
                );
198
            }
199
200
            if (!$previous->isSingleResult() && $identifier !== ODataConstants::URI_COUNT_SEGMENT) {
201
				throw ODataException::createBadRequestError(
202
                    Messages::segmentParserCannotQueryCollection($previous->getIdentifier())
203
                );
204
            }
205
206
            $current = new SegmentDescriptor();
207
            $current->setIdentifier($identifier);
208
            $current->setTargetSource(TargetSource::PROPERTY);
209
            $projectedProperty = $previous->getTargetResourceType()->resolveProperty($identifier);
210
            $current->setProjectedProperty($projectedProperty);
0 ignored issues
show
Bug introduced by
It seems like $projectedProperty defined by $previous->getTargetReso...veProperty($identifier) on line 209 can be null; however, POData\UriProcessor\Reso...:setProjectedProperty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
211
212
            if ($identifier === ODataConstants::URI_COUNT_SEGMENT) {
213
                if ($previousKind != TargetKind::RESOURCE()) {
214
					throw ODataException::createBadRequestError(
215
                        Messages::segmentParserCountCannotBeApplied($previous->getIdentifier())
216
                    );
217
                }
218
219
                if ($previous->isSingleResult()) {
220
					throw ODataException::createBadRequestError(
221
                        Messages::segmentParserCountCannotFollowSingleton($previous->getIdentifier())
222
                    );
223
                }
224
225
                $current->setTargetKind(TargetKind::PRIMITIVE_VALUE());
226
                $current->setSingleResult(true);
227
                $current->setTargetResourceSetWrapper(
228
                    $previous->getTargetResourceSetWrapper()
229
                );
230
                $current->setTargetResourceType(
231
                    $previous->getTargetResourceType()
232
                );
233
            } else if ($identifier === ODataConstants::URI_VALUE_SEGMENT
234
                && $previousKind == TargetKind::RESOURCE()
235
            ) {
236
                $current->setSingleResult(true);
237
                $current->setTargetResourceType(
238
                    $previous->getTargetResourceType()
239
                );
240
                $current->setTargetKind(TargetKind::MEDIA_RESOURCE());
241
            } else if (is_null($projectedProperty)) {
242
                if (!is_null($previous->getTargetResourceType())
243
                    && !is_null($previous->getTargetResourceType()->tryResolveNamedStreamByName($identifier))
244
                ) {
245
                    $current->setTargetKind(TargetKind::MEDIA_RESOURCE());
246
                    $current->setSingleResult(true);
247
                    $current->setTargetResourceType(
248
                        $previous->getTargetResourceType()
249
                    );
250
                } else {
251
					throw ODataException::createResourceNotFoundError($identifier);
252
                }
253
            } else {
254
                $current->setTargetResourceType($projectedProperty->getResourceType());
255
                $current->setSingleResult($projectedProperty->getKind() != ResourcePropertyKind::RESOURCESET_REFERENCE);
256
                if ($previousKind == TargetKind::LINK()
257
                    && $projectedProperty->getTypeKind() != ResourceTypeKind::ENTITY
258
                ) {
259
					throw ODataException::createBadRequestError(
260
                        Messages::segmentParserLinkSegmentMustBeFollowedByEntitySegment(
261
                            $identifier
262
                        )
263
                    );
264
                }
265
266
                switch($projectedProperty->getKind()) {
267
                    case ResourcePropertyKind::COMPLEX_TYPE:
268
                        $current->setTargetKind(TargetKind::COMPLEX_OBJECT());
269
                        break;
270
                    case ResourcePropertyKind::BAG | ResourcePropertyKind::PRIMITIVE:
271
                    case ResourcePropertyKind::BAG | ResourcePropertyKind::COMPLEX_TYPE:
272
                        $current->setTargetKind(TargetKind::BAG());
273
                        break;
274
                    case ResourcePropertyKind::RESOURCE_REFERENCE:
275
                    case ResourcePropertyKind::RESOURCESET_REFERENCE:
276
                        $current->setTargetKind(TargetKind::RESOURCE());
277
                        $resourceSetWrapper = $this->providerWrapper->getResourceSetWrapperForNavigationProperty($previous->getTargetResourceSetWrapper(), $previous->getTargetResourceType(), $projectedProperty);
278
                        if (is_null($resourceSetWrapper)) {
279
							throw ODataException::createResourceNotFoundError($projectedProperty->getName());
280
                        }
281
282
                        $current->setTargetResourceSetWrapper($resourceSetWrapper);
283
                        break;
284
                    default:
285
                        if (!$projectedProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
286
							throw ODataException::createInternalServerError(
287
                                Messages::segmentParserUnExpectedPropertyKind(
288
                                    'Primitive'
289
                                )
290
                            );
291
                        }
292
293
                        $current->setTargetKind(TargetKind::PRIMITIVE());
294
                        break;
295
                }
296
297
                if ($hasPredicate) {
298
                    $this->_assertion(!$current->isSingleResult());
299
                    $keyDescriptor = $this->_createKeyDescriptor(
300
                        $identifier . '(' . $keyPredicate . ')',
301
                        $projectedProperty->getResourceType(),
302
                        $keyPredicate
303
                    );
304
                    $current->setKeyDescriptor($keyDescriptor);
305
                    if (!$keyDescriptor->isEmpty()) {
306
                        $current->setSingleResult(true);
307
                    }
308
                }
309
310
                if ($checkRights&& !is_null($current->getTargetResourceSetWrapper())) {
311
                    $current->getTargetResourceSetWrapper()
312
                        ->checkResourceSetRightsForRead(
313
                            $current->isSingleResult()
314
                        );
315
                }
316
            }
317
        }
318
319
        return $current;
320
    }
321
322
    /**
323
     * Create SegmentDescriptor for the first segment
324
     * 
325
     * @param string $segmentIdentifier The identifier part of the first segment
326
     * @param string $keyPredicate The predicate part of the first segment if any else NULL
327
     * @param boolean $checkRights Whether to check the rights on this segment
328
     *
329
     * @return SegmentDescriptor Descriptor for the first segment
330
     * 
331
     * @throws ODataException Exception if any validation fails
332
     */
333
    private function _createFirstSegmentDescriptor($segmentIdentifier, $keyPredicate, $checkRights) {
334
        $descriptor = new SegmentDescriptor();
335
        $descriptor->setIdentifier($segmentIdentifier);
336
337 View Code Duplication
        if ($segmentIdentifier === ODataConstants::URI_METADATA_SEGMENT) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
338
            $this->_assertion(is_null($keyPredicate));            
339
            $descriptor->setTargetKind(TargetKind::METADATA());
340
            return $descriptor;
341
        }
342
343 View Code Duplication
        if ($segmentIdentifier === ODataConstants::URI_BATCH_SEGMENT) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
344
            $this->_assertion(is_null($keyPredicate));
345
            $descriptor->setTargetKind(TargetKind::BATCH());
346
            return $descriptor;
347
        }
348
349
        if ($segmentIdentifier === ODataConstants::URI_COUNT_SEGMENT) {
350
			throw ODataException::createBadRequestError(
351
                Messages::segmentParserSegmentNotAllowedOnRoot(
352
                    ODataConstants::URI_COUNT_SEGMENT
353
                )
354
            );
355
        }
356
357
        if ($segmentIdentifier === ODataConstants::URI_LINK_SEGMENT) {
358
			throw ODataException::createBadRequestError(
359
                Messages::segmentParserSegmentNotAllowedOnRoot(
360
                    ODataConstants::URI_LINK_SEGMENT
361
                )
362
            );
363
        }
364
365
        $resourceSetWrapper = $this->providerWrapper->resolveResourceSet($segmentIdentifier);
366
        if ($resourceSetWrapper === null) {
367
			throw ODataException::createResourceNotFoundError($segmentIdentifier);
368
        }
369
370
        $descriptor->setTargetResourceSetWrapper($resourceSetWrapper);
371
        $descriptor->setTargetResourceType($resourceSetWrapper->getResourceType());
372
        $descriptor->setTargetSource(TargetSource::ENTITY_SET);
373
        $descriptor->setTargetKind(TargetKind::RESOURCE());
374
        if ($keyPredicate !== null) {
375
            $keyDescriptor = $this->_createKeyDescriptor(
376
                $segmentIdentifier . '(' . $keyPredicate . ')', 
377
                $resourceSetWrapper->getResourceType(), 
378
                $keyPredicate
379
            );
380
            $descriptor->setKeyDescriptor($keyDescriptor);
381
            if (!$keyDescriptor->isEmpty()) {
382
                $descriptor->setSingleResult(true); 
383
            }
384
        }
385
386
        if ($checkRights) {
387
            $resourceSetWrapper->checkResourceSetRightsForRead(
388
                $descriptor->isSingleResult()
389
            );
390
        }
391
392
        return $descriptor;
393
    }
394
395
    /**
396
     * Creates an instance of KeyDescriptor by parsing a key predicate, also 
397
     * validates the KeyDescriptor
398
     * 
399
     * @param string       $segment      The uri segment in the form identifier
400
     *                                   (keyPredicate)
401
     * @param ResourceType $resourceType The Resource type whose keys need to 
402
     *                                   be parsed
403
     * @param string       $keyPredicate The key predicate to parse and generate 
404
     *                                   KeyDescriptor for
405
     * 
406
     * @return KeyDescriptor Describes the key values in the $keyPredicate
407
     * 
408
     * @throws ODataException Exception if any error occurs while parsing and 
409
     *                                  validating the key predicate
410
     */
411
    private function _createKeyDescriptor($segment, ResourceType $resourceType, $keyPredicate) {
412
        /**
413
         * @var KeyDescriptor $keyDescriptor
414
         */
415
        $keyDescriptor = null;
416
        if (!KeyDescriptor::tryParseKeysFromKeyPredicate($keyPredicate, $keyDescriptor)) {
417
            throw ODataException::createSyntaxError(Messages::syntaxError());
418
        }
419
        
420
        // Note: Currently WCF Data Service does not support multiple
421
        // 'Positional values' so Order_Details(10248, 11) is not valid
422
        if (!$keyDescriptor->isEmpty() 
423
            && !$keyDescriptor->areNamedValues() 
424
            && $keyDescriptor->valueCount() > 1
425
        ) {
426
            throw ODataException::createSyntaxError(
427
                Messages::segmentParserKeysMustBeNamed($segment)
428
            );
429
        }
430
431
432
        $keyDescriptor->validate($segment, $resourceType);
433
            
434
435
        return $keyDescriptor;
436
    }
437
438
    /**
439
     * Assert that the given condition is true, if false throw 
440
     * ODataException for syntax error
441
     * 
442
     * @param boolean $condition The condition to assert
443
     * 
444
     * @return void
445
     * 
446
     * @throws ODataException
447
     */
448
    private function _assertion($condition)
449
    {
450
        if (!$condition) {
451
            throw ODataException::createSyntaxError(Messages::syntaxError());
452
        }
453
    }
454
}