Completed
Push — master ( 561959...44ef42 )
by Alex
02:58
created

KeyDescriptor::getPositionalValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace POData\UriProcessor\ResourcePathProcessor\SegmentParser;
4
5
use POData\Common\InvalidOperationException;
6
use POData\Common\Messages;
7
use POData\Common\ODataException;
8
use POData\Providers\Metadata\ResourceSet;
9
use POData\Providers\Metadata\ResourceType;
10
use POData\Providers\Metadata\Type\Boolean;
11
use POData\Providers\Metadata\Type\DateTime;
12
use POData\Providers\Metadata\Type\Decimal;
13
use POData\Providers\Metadata\Type\Double;
14
use POData\Providers\Metadata\Type\Guid;
15
use POData\Providers\Metadata\Type\Int32;
16
use POData\Providers\Metadata\Type\Int64;
17
use POData\Providers\Metadata\Type\IType;
18
use POData\Providers\Metadata\Type\Null1;
19
use POData\Providers\Metadata\Type\Single;
20
use POData\Providers\Metadata\Type\StringType;
21
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
22
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
23
24
/**
25
 * Class KeyDescriptor.
26
 *
27
 * A type used to represent Key (identifier) for an entity (resource), This class
28
 * can parse an Astoria KeyPredicate, KeyPredicate will be in one of the following
29
 * two formats:
30
 *  1) KeyValue                                      : If the Entry has a single key
31
 *                                                     Property the predicate may
32
 *                                                     include only the value of the
33
 *                                                     key Property.
34
 *      e.g. 'ALFKI' in Customers('ALFKI')
35
 *  2) Property = KeyValue [, Property = KeyValue]*  : If the key is made up of two
36
 *                                                     or more Properties, then its
37
 *                                                     value must be stated using
38
 *                                                     name/value pairs.
39
 *      e.g. 'ALFKI' in Customers(CustomerID = 'ALFKI'),
40
 *          "OrderID=10248,ProductID=11" in Order_Details(OrderID=10248,ProductID=11)
41
 *
42
 * Entity's identifier is a collection of value for key properties. These values
43
 * can be named or positional, depending on how they were specified in the URI.
44
 *  e.g. Named values:
45
 *         Customers(CustomerID = 'ALFKI'), Order_Details(OrderID=10248,ProductID=11)
46
 *       Positional values:
47
 *         Customers('ALFKI'), Order_Details(10248, 11)
48
 * Note: Currently WCF Data Service does not support multiple 'Positional values' so
49
 *       Order_Details(10248, 11) is not valid, but this class can parse both types.
50
 * Note: This type is also used to parse and validate skiptoken value as they are
51
 *       comma separated positional values.
52
 */
53
class KeyDescriptor
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
54
{
55
    /**
56
     * Holds collection of named key values
57
     * For e.g. the keypredicate Order_Details(OrderID=10248,ProductID=11) will
58
     * stored in this array as:
59
     * Array([OrderID] => Array( [0] => 10248 [1] => Object(Int32)),
60
     *       [ProductID] => Array( [0] => 11 [1] => Object(Int32)))
61
     * Note: This is mutually exclusive with $_positionalValues. These values
62
     * are not validated against entity's ResourceType, validation will happen
63
     * once validate function is called, $_validatedNamedValues will hold
64
     * validated values.
65
     *
66
     * @var array
67
     */
68
    private $namedValues = [];
69
70
    /**
71
     * Holds collection of positional key values
72
     * For e.g. the keypredicate Order_Details(10248, 11) will
73
     * stored in this array as:
74
     * Array([0] => Array( [0] => 10248 [1] => Object(Int32)),
75
     *       [1] => Array( [0] => 11 [1] => Object(Int32)))
76
     * Note: This is mutually exclusive with $_namedValues. These values are not
77
     * validated against entity's ResourceType, validation will happen once validate
78
     * function is called, $_validatedNamedValues will hold validated values.
79
     *
80
     * @var array
81
     */
82
    private $positionalValues = [];
83
84
    /**
85
     * Holds collection of positional or named values as named values. The validate
86
     * function populates this collection.
87
     *
88
     * @var array
89
     */
90
    private $validatedNamedValues = [];
91
92
    /**
93
     * Creates new instance of KeyDescriptor
94
     * Note: The arguments $namedValues and $positionalValues are mutually
95
     * exclusive. Either both or one will be empty array.
96
     *
97
     * @param array $namedValues      Collection of named key values
98
     * @param array $positionalValues Collection of positional key values
99
     */
100
    private function __construct(array $namedValues, array $positionalValues)
101
    {
102
        $this->namedValues = $namedValues;
103
        $this->positionalValues = $positionalValues;
104
        $this->validatedNamedValues = [];
105
    }
106
107
    /**
108
     * @param string $keyString
109
     * @param bool $isKey
110
     * @param KeyDescriptor|null $keyDescriptor
111
     * @return bool
112
     */
113
    protected static function parseAndVerifyRawKeyPredicate($keyString, $isKey, KeyDescriptor &$keyDescriptor = null)
114
    {
115
        $result = self::tryParseKeysFromRawKeyPredicate(
116
            $keyString,
117
            $isKey,
118
            !$isKey,
119
            $keyDescriptor
120
        );
121
        assert(true === $result || false === $result, 'Result must be boolean');
122
        assert($result === isset($keyDescriptor), 'Result must match existence of keyDescriptor');
123
        return $result;
124
    }
125
126
    /**
127
     * Gets collection of named key values.
128
     *
129
     * @return array[]
130
     */
131
    public function getNamedValues()
132
    {
133
        return $this->namedValues;
134
    }
135
136
    /**
137
     * Gets collection of positional key values.
138
     *
139
     * @return array[]
140
     */
141
    public function getPositionalValues()
142
    {
143
        return $this->positionalValues;
144
    }
145
146
    /**
147
     * Gets collection of positional key values by reference.
148
     *
149
     * @return array[]
150
     */
151
    public function &getPositionalValuesByRef()
152
    {
153
        return $this->positionalValues;
154
    }
155
156
    /**
157
     * Gets validated named key values, this array will be populated
158
     * in validate function.
159
     *
160
     * @throws InvalidOperationException If this function invoked before invoking validate function
161
     *
162
     * @return array[]
163
     */
164
    public function getValidatedNamedValues()
165
    {
166
        if (empty($this->validatedNamedValues)) {
167
            throw new InvalidOperationException(
168
                Messages::keyDescriptorValidateNotCalled()
169
            );
170
        }
171
172
        return $this->validatedNamedValues;
173
    }
174
175
    /**
176
     * Checks whether the key values have name.
177
     *
178
     * @return bool
179
     */
180
    public function areNamedValues()
0 ignored issues
show
Coding Style introduced by
function areNamedValues() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
181
    {
182
        return !empty($this->namedValues);
183
    }
184
185
    /**
186
     * Check whether this KeyDesciption has any key values.
187
     *
188
     * @return bool
189
     */
190
    public function isEmpty()
191
    {
192
        return empty($this->namedValues)
193
             && empty($this->positionalValues);
194
    }
195
196
    /**
197
     * Gets number of values in the key.
198
     *
199
     * @return int
200
     */
201
    public function valueCount()
202
    {
203
        if ($this->isEmpty()) {
204
            return 0;
205
        } elseif (!empty($this->namedValues)) {
206
            return count($this->namedValues);
207
        }
208
209
        return count($this->positionalValues);
210
    }
211
212
    /**
213
     * Attempts to parse value(s) of resource key(s) from the given key predicate
214
     *  and creates instance of KeyDescription representing the same, Once parsing
215
     *  is done one should call validate function to validate the created
216
     *  KeyDescription.
217
     *
218
     * @param string             $keyPredicate  The predicate to parse
219
     * @param KeyDescriptor|null $keyDescriptor On return, Description of key after parsing
220
     *
221
     * @return bool True if the given values were parsed; false if there was a syntax error
222
     */
223
    public static function tryParseKeysFromKeyPredicate(
0 ignored issues
show
Coding Style introduced by
function tryParseKeysFromKeyPredicate() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
224
        $keyPredicate,
225
        KeyDescriptor &$keyDescriptor = null
226
    ) {
227
        $isKey = true;
228
        $keyString = $keyPredicate;
229
        return self::parseAndVerifyRawKeyPredicate($keyString, $isKey, $keyDescriptor);
230
    }
231
232
    /**
233
     * Attempt to parse comma separated values representing a skiptoken and creates
234
     * instance of KeyDescriptor representing the same.
235
     *
236
     * @param string        $skipToken      The skiptoken value to parse
237
     * @param KeyDescriptor &$keyDescriptor On return, Description of values
238
     *                                      after parsing
239
     *
240
     * @return bool True if the given values were parsed; false if there was a syntax error
241
     */
242
    public static function tryParseValuesFromSkipToken($skipToken, &$keyDescriptor)
0 ignored issues
show
Coding Style introduced by
function tryParseValuesFromSkipToken() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
243
    {
244
        $isKey = false;
245
        $keyString = $skipToken;
246
        return self::parseAndVerifyRawKeyPredicate($keyString, $isKey, $keyDescriptor);
247
    }
248
249
    /**
250
     * Validate this KeyDescriptor, If valid, this function populates
251
     * _validatedNamedValues array with key as keyName and value as an array of
252
     * key value and key type.
253
     *
254
     * @param string       $segmentAsString The segment in the form identifier
255
     *                                      (keyPredicate) which this descriptor
256
     *                                      represents
257
     * @param ResourceType $resourceType    The type of the identifier in the segment
258
     *
259
     * @throws ODataException If validation fails
260
     */
261
    public function validate($segmentAsString, ResourceType $resourceType)
262
    {
263
        if ($this->isEmpty()) {
264
            $this->validatedNamedValues = [];
265
266
            return;
267
        }
268
269
        $keyProperties = $resourceType->getKeyProperties();
270
        $keyPropertiesCount = count($keyProperties);
271
        if (!empty($this->namedValues)) {
272 View Code Duplication
            if (count($this->namedValues) != $keyPropertiesCount) {
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...
273
                throw ODataException::createSyntaxError(
274
                    Messages::keyDescriptorKeyCountNotMatching(
275
                        $segmentAsString,
276
                        $keyPropertiesCount,
277
                        count($this->namedValues)
278
                    )
279
                );
280
            }
281
282
            foreach ($keyProperties as $keyName => $keyResourceProperty) {
283
                if (!array_key_exists($keyName, $this->namedValues)) {
284
                    $keysAsString = null;
285
                    foreach (array_keys($keyProperties) as $key) {
286
                        $keysAsString .= $key . ', ';
287
                    }
288
289
                    $keysAsString = rtrim($keysAsString, ' ,');
290
                    throw ODataException::createSyntaxError(
291
                        Messages::keyDescriptorMissingKeys(
292
                            $segmentAsString,
293
                            $keysAsString
294
                        )
295
                    );
296
                }
297
298
                $typeProvided = $this->namedValues[$keyName][1];
299
                $expectedType = $keyResourceProperty->getInstanceType();
300
                assert($expectedType instanceof IType, get_class($expectedType));
301 View Code Duplication
                if (!$expectedType->isCompatibleWith($typeProvided)) {
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...
302
                    throw ODataException::createSyntaxError(
303
                        Messages::keyDescriptorInCompatibleKeyType(
304
                            $segmentAsString,
305
                            $keyName,
306
                            $expectedType->getFullTypeName(),
307
                            $typeProvided->getFullTypeName()
308
                        )
309
                    );
310
                }
311
312
                $this->validatedNamedValues[$keyName] = $this->namedValues[$keyName];
313
            }
314
        } else {
315 View Code Duplication
            if (count($this->positionalValues) != $keyPropertiesCount) {
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...
316
                throw ODataException::createSyntaxError(
317
                    Messages::keyDescriptorKeyCountNotMatching(
318
                        $segmentAsString,
319
                        $keyPropertiesCount,
320
                        count($this->positionalValues)
321
                    )
322
                );
323
            }
324
325
            $i = 0;
326
            foreach ($keyProperties as $keyName => $keyResourceProperty) {
327
                $typeProvided = $this->positionalValues[$i][1];
328
                $expectedType = $keyResourceProperty->getInstanceType();
329
                assert($expectedType instanceof IType, get_class($expectedType));
330
331 View Code Duplication
                if (!$expectedType->isCompatibleWith($typeProvided)) {
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...
332
                    throw ODataException::createSyntaxError(
333
                        Messages::keyDescriptorInCompatibleKeyTypeAtPosition(
334
                            $segmentAsString,
335
                            $keyResourceProperty->getName(),
336
                            $i,
337
                            $expectedType->getFullTypeName(),
338
                            $typeProvided->getFullTypeName()
339
                        )
340
                    );
341
                }
342
343
                $this->validatedNamedValues[$keyName]
344
                    = $this->positionalValues[$i];
345
                ++$i;
346
            }
347
        }
348
    }
349
350
    /**
351
     * Attempts to parse value(s) of resource key(s) from the key predicate and
352
     * creates instance of KeyDescription representing the same, Once parsing is
353
     * done, one should call validate function to validate the created KeyDescription.
354
     *
355
     * @param string        $keyPredicate     The key predicate to parse
356
     * @param bool          $allowNamedValues Set to true if parser should accept
357
     *                                        named values(Property = KeyValue),
358
     *                                        if false then parser will fail on
359
     *                                        such constructs
360
     * @param bool          $allowNull        Set to true if parser should accept
361
     *                                        null values for positional key
362
     *                                        values, if false then parser will
363
     *                                        fail on seeing null values
364
     * @param KeyDescriptor &$keyDescriptor   On return, Description of key after
365
     *                                        parsing
366
     *
367
     * @return bool True if the given values were parsed; false if there was a syntax error
368
     */
369
    private static function tryParseKeysFromRawKeyPredicate(
0 ignored issues
show
Coding Style introduced by
function tryParseKeysFromRawKeyPredicate() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
370
        $keyPredicate,
371
        $allowNamedValues,
372
        $allowNull,
373
        &$keyDescriptor
374
    ) {
375
        $expressionLexer = new ExpressionLexer($keyPredicate);
376
        $currentToken = $expressionLexer->getCurrentToken();
377
378
        //Check for empty predicate e.g. Customers(  )
379
        if ($currentToken->Id == ExpressionTokenId::END) {
380
            $keyDescriptor = new self([], []);
381
382
            return true;
383
        }
384
385
        $namedValues = [];
386
        $positionalValues = [];
387
388
        do {
389
            if (($currentToken->Id == ExpressionTokenId::IDENTIFIER)
390
                && $allowNamedValues
391
            ) {
392
                //named and positional values are mutually exclusive
393
                if (!empty($positionalValues)) {
394
                    return false;
395
                }
396
397
                //expecting keyName=keyValue, verify it
398
                $identifier = $currentToken->getIdentifier();
399
                $expressionLexer->nextToken();
400
                $currentToken = $expressionLexer->getCurrentToken();
401
                if ($currentToken->Id != ExpressionTokenId::EQUAL) {
402
                    return false;
403
                }
404
405
                $expressionLexer->nextToken();
406
                $currentToken = $expressionLexer->getCurrentToken();
407
                if (!$currentToken->isKeyValueToken()) {
408
                    return false;
409
                }
410
411
                if (array_key_exists($identifier, $namedValues)) {
412
                    //Duplication of KeyName not allowed
413
                    return false;
414
                }
415
416
                //Get type of keyValue and validate keyValue
417
                $outValue = $outType = null;
418
                if (!self::getTypeAndValidateKeyValue(
419
                    $currentToken->Text,
420
                    $currentToken->Id,
421
                    $outValue,
422
                    $outType
423
                )
424
                ) {
425
                    return false;
426
                }
427
428
                $namedValues[$identifier] = [$outValue, $outType];
429
            } elseif ($currentToken->isKeyValueToken()
430
                || ($currentToken->Id == ExpressionTokenId::NULL_LITERAL && $allowNull)
431
            ) {
432
                //named and positional values are mutually exclusive
433
                if (!empty($namedValues)) {
434
                    return false;
435
                }
436
437
                //Get type of keyValue and validate keyValue
438
                $outValue = $outType = null;
439
                if (!self::getTypeAndValidateKeyValue(
440
                    $currentToken->Text,
441
                    $currentToken->Id,
442
                    $outValue,
443
                    $outType
444
                )
445
                ) {
446
                    return false;
447
                }
448
449
                $positionalValues[] = [$outValue, $outType];
450
            } else {
451
                return false;
452
            }
453
454
            $expressionLexer->nextToken();
455
            $currentToken = $expressionLexer->getCurrentToken();
456
            if ($currentToken->Id == ExpressionTokenId::COMMA) {
457
                $expressionLexer->nextToken();
458
                $currentToken = $expressionLexer->getCurrentToken();
459
                //end of text and comma, Trailing comma not allowed
460
                if ($currentToken->Id == ExpressionTokenId::END) {
461
                    return false;
462
                }
463
            }
464
        } while ($currentToken->Id != ExpressionTokenId::END);
465
466
        $keyDescriptor = new self($namedValues, $positionalValues);
467
468
        return true;
469
    }
470
471
    /**
472
     * Get the type of an Astoria URI key value, validate the value against the type. If valid, this function
473
     * provides the PHP value equivalent to the Astoria URI key value.
474
     *
475
     * @param string            $value     The Astoria URI key value
476
     * @param ExpressionTokenId $tokenId   The tokenId for $value literal
477
     * @param mixed|null        &$outValue After the invocation, this parameter holds the PHP equivalent to $value,
478
     *                                     if $value is not valid then this parameter will be null
479
     * @param IType|null        &$outType  After the invocation, this parameter holds the type of $value, if $value is
480
     *                                     not a valid key value type then this parameter will be null
481
     *
482
     * @return bool True if $value is a valid type, else false
483
     */
484
    private static function getTypeAndValidateKeyValue($value, $tokenId, &$outValue, &$outType)
0 ignored issues
show
Coding Style introduced by
function getTypeAndValidateKeyValue() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
485
    {
486
        switch ($tokenId) {
487
            case ExpressionTokenId::BOOLEAN_LITERAL:
488
                $outType = new Boolean();
489
                break;
490
            case ExpressionTokenId::DATETIME_LITERAL:
491
                $outType = new DateTime();
492
                break;
493
            case ExpressionTokenId::GUID_LITERAL:
494
                $outType = new Guid();
495
                break;
496
            case ExpressionTokenId::STRING_LITERAL:
497
                $outType = new StringType();
498
                break;
499
            case ExpressionTokenId::INTEGER_LITERAL:
500
                $outType = new Int32();
501
                break;
502
            case ExpressionTokenId::DECIMAL_LITERAL:
503
                $outType = new Decimal();
504
                break;
505
            case ExpressionTokenId::DOUBLE_LITERAL:
506
                $outType = new Double();
507
                break;
508
            case ExpressionTokenId::INT64_LITERAL:
509
                $outType = new Int64();
510
                break;
511
            case ExpressionTokenId::SINGLE_LITERAL:
512
                $outType = new Single();
513
                break;
514
            case ExpressionTokenId::NULL_LITERAL:
515
                $outType = new Null1();
516
                break;
517
            default:
518
                $outType = null;
519
520
                return false;
521
        }
522
523
        if (!$outType->validate($value, $outValue)) {
524
            $outType = $outValue = null;
525
526
            return false;
527
        }
528
529
        return true;
530
    }
531
532
    /**
533
     * Generate relative edit url for this key descriptor and supplied resource set
534
     *
535
     * @param ResourceSet $resourceSet
536
     *
537
     * @return string
538
     * @throws \InvalidArgumentException
539
     */
540
    public function generateRelativeUri(ResourceSet $resourceSet)
541
    {
542
        $resourceType = $resourceSet->getResourceType();
543
        $keys = $resourceType->getKeyProperties();
544
545
        $namedKeys = $this->getNamedValues();
546
        assert(0 !== count($keys), 'count($keys) == 0');
547
        if (count($keys) !== count($namedKeys)) {
548
            $msg = 'Mismatch between supplied key predicates and number of keys defined on resource set';
549
            throw new \InvalidArgumentException($msg);
550
        }
551
        $editUrl = $resourceSet->getName() . '(';
552
        $comma = null;
553
        foreach ($keys as $keyName => $resourceProperty) {
554
            if (!array_key_exists($keyName, $namedKeys)) {
555
                $msg = 'Key predicate '.$keyName.' not present in named values';
556
                throw new \InvalidArgumentException($msg);
557
            }
558
            $keyType = $resourceProperty->getInstanceType();
559
            assert($keyType instanceof IType, '$keyType not instanceof IType');
560
            $keyValue = $namedKeys[$keyName][0];
561
            $keyValue = $keyType->convertToOData($keyValue);
562
563
            $editUrl .= $comma . $keyName . '=' . $keyValue;
564
            $comma = ',';
565
        }
566
567
        $editUrl .= ')';
568
569
        return $editUrl;
570
    }
571
}
572