Completed
Push — master ( 54ec6b...e26cc9 )
by Alex
19s queued 13s
created

KeyDescriptor::toNextLexerToken()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\UriProcessor\ResourcePathProcessor\SegmentParser;
6
7
use InvalidArgumentException;
8
use POData\Common\InvalidOperationException;
9
use POData\Common\Messages;
10
use POData\Common\ODataException;
11
use POData\ObjectModel\ODataProperty;
12
use POData\Providers\Metadata\ResourceSet;
13
use POData\Providers\Metadata\ResourceType;
14
use POData\Providers\Metadata\Type\Boolean;
15
use POData\Providers\Metadata\Type\DateTime;
16
use POData\Providers\Metadata\Type\Decimal;
17
use POData\Providers\Metadata\Type\Double;
18
use POData\Providers\Metadata\Type\Guid;
19
use POData\Providers\Metadata\Type\Int32;
20
use POData\Providers\Metadata\Type\Int64;
21
use POData\Providers\Metadata\Type\IType;
22
use POData\Providers\Metadata\Type\Null1;
23
use POData\Providers\Metadata\Type\Single;
24
use POData\Providers\Metadata\Type\StringType;
25
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
26
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionToken;
27
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
28
use ReflectionException;
29
30
/**
31
 * Class KeyDescriptor.
32
 *
33
 * A type used to represent Key (identifier) for an entity (resource), This class
34
 * can parse an Astoria KeyPredicate, KeyPredicate will be in one of the following
35
 * two formats:
36
 *  1) KeyValue                                      : If the Entry has a single key
37
 *                                                     Property the predicate may
38
 *                                                     include only the value of the
39
 *                                                     key Property.
40
 *      e.g. 'ALFKI' in Customers('ALFKI')
41
 *  2) Property = KeyValue [, Property = KeyValue]*  : If the key is made up of two
42
 *                                                     or more Properties, then its
43
 *                                                     value must be stated using
44
 *                                                     name/value pairs.
45
 *      e.g. 'ALFKI' in Customers(CustomerID = 'ALFKI'),
46
 *          "OrderID=10248,ProductID=11" in Order_Details(OrderID=10248,ProductID=11)
47
 *
48
 * Entity's identifier is a collection of value for key properties. These values
49
 * can be named or positional, depending on how they were specified in the URI.
50
 *  e.g. Named values:
51
 *         Customers(CustomerID = 'ALFKI'), Order_Details(OrderID=10248,ProductID=11)
52
 *       Positional values:
53
 *         Customers('ALFKI'), Order_Details(10248, 11)
54
 * Note: Currently WCF Data Service does not support multiple 'Positional values' so
55
 *       Order_Details(10248, 11) is not valid, but this class can parse both types.
56
 * Note: This type is also used to parse and validate skiptoken value as they are
57
 *       comma separated positional values.
58
 */
59
class KeyDescriptor
60
{
61
    /**
62
     * Holds collection of named key values
63
     * For e.g. the keypredicate Order_Details(OrderID=10248,ProductID=11) will
64
     * stored in this array as:
65
     * Array([OrderID] => Array( [0] => 10248 [1] => Object(Int32)),
66
     *       [ProductID] => Array( [0] => 11 [1] => Object(Int32)))
67
     * Note: This is mutually exclusive with $_positionalValues. These values
68
     * are not validated against entity's ResourceType, validation will happen
69
     * once validate function is called, $_validatedNamedValues will hold
70
     * validated values.
71
     *
72
     * @var array
73
     */
74
    private $namedValues = [];
75
76
    /**
77
     * Holds collection of positional key values
78
     * For e.g. the keypredicate Order_Details(10248, 11) will
79
     * stored in this array as:
80
     * Array([0] => Array( [0] => 10248 [1] => Object(Int32)),
81
     *       [1] => Array( [0] => 11 [1] => Object(Int32)))
82
     * Note: This is mutually exclusive with $_namedValues. These values are not
83
     * validated against entity's ResourceType, validation will happen once validate
84
     * function is called, $_validatedNamedValues will hold validated values.
85
     *
86
     * @var array
87
     */
88
    private $positionalValues = [];
89
90
    /**
91
     * Holds collection of positional or named values as named values. The validate
92
     * function populates this collection.
93
     *
94
     * @var array
95
     */
96
    private $validatedNamedValues = [];
97
98
    /**
99
     * Creates new instance of KeyDescriptor
100
     * Note: The arguments $namedValues and $positionalValues are mutually
101
     * exclusive. Either both or one will be empty array.
102
     *
103
     * @param array $namedValues      Collection of named key values
104
     * @param array $positionalValues Collection of positional key values
105
     */
106
    private function __construct(array $namedValues, array $positionalValues)
107
    {
108
        $namedCount = count($namedValues);
109
        $posCount   = count($positionalValues);
110
        assert(0 == min($namedCount, $posCount), 'At least one of named and positional values arrays must be empty');
111
        if (0 < $namedCount) {
112
            $keys = array_keys($namedValues);
113
            for ($i = 0; $i < $namedCount; $i++) {
114
                $namedValues[$keys[$i]][0] = urldecode($namedValues[$keys[$i]][0]);
115
            }
116
        }
117
        if (0 < $posCount) {
118
            for ($i = 0; $i < $posCount; $i++) {
119
                $positionalValues[$i][0] = urldecode($positionalValues[$i][0]);
120
            }
121
        }
122
        $this->namedValues          = $namedValues;
123
        $this->positionalValues     = $positionalValues;
124
        $this->validatedNamedValues = [];
125
    }
126
127
    /**
128
     * Attempts to parse value(s) of resource key(s) from the given key predicate
129
     *  and creates instance of KeyDescription representing the same, Once parsing
130
     *  is done one should call validate function to validate the created
131
     *  KeyDescription.
132
     *
133
     * @param string             $keyPredicate  The predicate to parse
134
     * @param KeyDescriptor|null $keyDescriptor On return, Description of key after parsing
135
     *
136
     * @throws ODataException
137
     * @return bool           True if the given values were parsed; false if there was a syntax error
138
     */
139
    public static function tryParseKeysFromKeyPredicate(
140
        string $keyPredicate,
141
        KeyDescriptor &$keyDescriptor = null
142
    ): bool {
143
        $isKey     = true;
144
        $keyString = $keyPredicate;
145
        return self::parseAndVerifyRawKeyPredicate($keyString, $isKey, $keyDescriptor);
146
    }
147
148
    /**
149
     * @param  string             $keyString
150
     * @param  bool               $isKey
151
     * @param  KeyDescriptor|null $keyDescriptor
152
     * @throws ODataException
153
     * @return bool
154
     */
155
    protected static function parseAndVerifyRawKeyPredicate(
156
        string $keyString,
157
        bool $isKey,
158
        KeyDescriptor &$keyDescriptor = null
159
    ): bool {
160
        $result = self::tryParseKeysFromRawKeyPredicate(
161
            $keyString,
162
            $isKey,
163
            !$isKey,
164
            $keyDescriptor
165
        );
166
        assert($result === isset($keyDescriptor), 'Result must match existence of keyDescriptor');
167
        return $result;
168
    }
169
170
    /**
171
     * Attempts to parse value(s) of resource key(s) from the key predicate and
172
     * creates instance of KeyDescription representing the same, Once parsing is
173
     * done, one should call validate function to validate the created KeyDescription.
174
     *
175
     * @param string        $keyPredicate     The key predicate to parse
176
     * @param bool          $allowNamedValues Set to true if parser should accept
177
     *                                        named values(Property = KeyValue),
178
     *                                        if false then parser will fail on
179
     *                                        such constructs
180
     * @param bool          $allowNull        Set to true if parser should accept
181
     *                                        null values for positional key
182
     *                                        values, if false then parser will
183
     *                                        fail on seeing null values
184
     * @param KeyDescriptor &$keyDescriptor   On return, Description of key after
185
     *                                        parsing
186
     *
187
     * @throws ODataException
188
     * @return bool           True if the given values were parsed; false if there was a syntax error
189
     */
190
    private static function tryParseKeysFromRawKeyPredicate(
191
        string $keyPredicate,
192
        bool $allowNamedValues,
193
        bool $allowNull,
194
        ?KeyDescriptor &$keyDescriptor
195
    ): bool {
196
        $expressionLexer = new ExpressionLexer($keyPredicate);
197
        $currentToken    = $expressionLexer->getCurrentToken();
198
199
        //Check for empty predicate e.g. Customers(  )
200
        if ($currentToken->getId() == ExpressionTokenId::END()) {
201
            $keyDescriptor = new self([], []);
202
203
            return true;
204
        }
205
206
        $namedValues      = [];
207
        $positionalValues = [];
208
209
        do {
210
            if (($currentToken->getId() == ExpressionTokenId::IDENTIFIER())
211
                && $allowNamedValues
212
            ) {
213
                //named and positional values are mutually exclusive
214
                if (!empty($positionalValues)) {
215
                    return false;
216
                }
217
218
                //expecting keyName=keyValue, verify it
219
                $identifier = $currentToken->getIdentifier();
220
                $currentToken = self::toNextLexerToken($expressionLexer);
221
                if ($currentToken->getId() != ExpressionTokenId::EQUAL()) {
222
                    return false;
223
                }
224
225
                $currentToken = self::toNextLexerToken($expressionLexer);
226
                if (!$currentToken->isKeyValueToken()) {
227
                    return false;
228
                }
229
230
                if (array_key_exists($identifier, $namedValues)) {
231
                    //Duplication of KeyName not allowed
232
                    return false;
233
                }
234
235
                //Get type of keyValue and validate keyValue
236
                $outValue = $outType = null;
237
                if (!self::getTypeAndValidateKeyValue(
238
                    $currentToken->Text,
239
                    $currentToken->getId(),
240
                    $outValue,
241
                    $outType
242
                )
243
                ) {
244
                    return false;
245
                }
246
247
                $namedValues[$identifier] = [$outValue, $outType];
248
            } elseif ($currentToken->isKeyValueToken()
249
                || ($currentToken->getId() == ExpressionTokenId::NULL_LITERAL() && $allowNull)
250
            ) {
251
                //named and positional values are mutually exclusive
252
                if (!empty($namedValues)) {
253
                    return false;
254
                }
255
256
                //Get type of keyValue and validate keyValue
257
                $outValue = $outType = null;
258
                if (!self::getTypeAndValidateKeyValue(
259
                    $currentToken->Text,
260
                    $currentToken->getId(),
261
                    $outValue,
262
                    $outType
263
                )
264
                ) {
265
                    return false;
266
                }
267
268
                $positionalValues[] = [$outValue, $outType];
269
            } else {
270
                return false;
271
            }
272
273
            $currentToken = self::toNextLexerToken($expressionLexer);
274
            if ($currentToken->getId() == ExpressionTokenId::COMMA()) {
275
                $currentToken = self::toNextLexerToken($expressionLexer);
276
                //end of text and comma, Trailing comma not allowed
277
                if ($currentToken->getId() == ExpressionTokenId::END()) {
278
                    return false;
279
                }
280
            }
281
        } while ($currentToken->getId() != ExpressionTokenId::END());
282
283
        $keyDescriptor = new self($namedValues, $positionalValues);
284
285
        return true;
286
    }
287
288
    /**
289
     * Get the type of an Astoria URI key value, validate the value against the type. If valid, this function
290
     * provides the PHP value equivalent to the Astoria URI key value.
291
     *
292
     * @param string            $value     The Astoria URI key value
293
     * @param ExpressionTokenId $tokenId   The tokenId for $value literal
294
     * @param mixed|null        &$outValue After the invocation, this parameter holds the PHP equivalent to $value,
295
     *                                     if $value is not valid then this parameter will be null
296
     * @param IType|null        &$outType  After the invocation, this parameter holds the type of $value, if $value is
297
     *                                     not a valid key value type then this parameter will be null
298
     *
299
     * @return bool True if $value is a valid type, else false
300
     */
301
    private static function getTypeAndValidateKeyValue(
302
        string $value,
303
        ExpressionTokenId $tokenId,
304
        &$outValue,
305
        IType &$outType = null
306
    ): bool {
307
        switch ($tokenId) {
308
            case ExpressionTokenId::BOOLEAN_LITERAL():
309
                $outType = new Boolean();
310
                break;
311
            case ExpressionTokenId::DATETIME_LITERAL():
312
                $outType = new DateTime();
313
                break;
314
            case ExpressionTokenId::GUID_LITERAL():
315
                $outType = new Guid();
316
                break;
317
            case ExpressionTokenId::STRING_LITERAL():
318
                $outType = new StringType();
319
                break;
320
            case ExpressionTokenId::INTEGER_LITERAL():
321
                $outType = new Int32();
322
                break;
323
            case ExpressionTokenId::DECIMAL_LITERAL():
324
                $outType = new Decimal();
325
                break;
326
            case ExpressionTokenId::DOUBLE_LITERAL():
327
                $outType = new Double();
328
                break;
329
            case ExpressionTokenId::INT64_LITERAL():
330
                $outType = new Int64();
331
                break;
332
            case ExpressionTokenId::SINGLE_LITERAL():
333
                $outType = new Single();
334
                break;
335
            case ExpressionTokenId::NULL_LITERAL():
336
                $outType = new Null1();
337
                break;
338
            default:
339
                $outType = null;
340
341
                return false;
342
        }
343
344
        if (!$outType->validate($value, $outValue)) {
345
            $outType = $outValue = null;
346
347
            return false;
348
        }
349
350
        return true;
351
    }
352
353
    /**
354
     * Attempt to parse comma separated values representing a skiptoken and creates
355
     * instance of KeyDescriptor representing the same.
356
     *
357
     * @param string        $skipToken      The skiptoken value to parse
358
     * @param KeyDescriptor &$keyDescriptor On return, Description of values
359
     *                                      after parsing
360
     *
361
     * @throws ODataException
362
     * @return bool           True if the given values were parsed; false if there was a syntax error
363
     */
364
    public static function tryParseValuesFromSkipToken(string $skipToken, ?KeyDescriptor &$keyDescriptor): bool
365
    {
366
        $isKey     = false;
367
        $keyString = $skipToken;
368
        return self::parseAndVerifyRawKeyPredicate($keyString, $isKey, $keyDescriptor);
369
    }
370
371
    /**
372
     * @param ExpressionLexer $expressionLexer
373
     * @return \POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionToken
374
     * @throws ODataException
375
     */
376
    private static function toNextLexerToken(ExpressionLexer $expressionLexer): ExpressionToken
377
    {
378
        $expressionLexer->nextToken();
379
        return $expressionLexer->getCurrentToken();
380
    }
381
382
    /**
383
     * Gets collection of positional key values.
384
     *
385
     * @return array[]
386
     */
387
    public function getPositionalValues(): array
388
    {
389
        return $this->positionalValues;
390
    }
391
392
    /**
393
     * Gets collection of positional key values by reference.
394
     *
395
     * @return array[]
396
     */
397
    public function &getPositionalValuesByRef(): array
398
    {
399
        return $this->positionalValues;
400
    }
401
402
    /**
403
     * Checks whether the key values have name.
404
     *
405
     * @return bool
406
     */
407
    public function areNamedValues(): bool
408
    {
409
        return !empty($this->namedValues);
410
    }
411
412
    /**
413
     * Gets number of values in the key.
414
     *
415
     * @return int
416
     */
417
    public function valueCount(): int
418
    {
419
        return !empty($this->namedValues) ? count($this->namedValues) : count($this->positionalValues);
420
    }
421
422
    /**
423
     * Check whether this KeyDescription has any key values.
424
     *
425
     * @return bool
426
     */
427
    public function isEmpty(): bool
428
    {
429
        return empty($this->namedValues) && empty($this->positionalValues);
430
    }
431
432
    /**
433
     * Validate this KeyDescriptor, If valid, this function populates
434
     * _validatedNamedValues array with key as keyName and value as an array of
435
     * key value and key type.
436
     *
437
     * @param string       $segmentAsString The segment in the form identifier
438
     *                                      (keyPredicate) which this descriptor
439
     *                                      represents
440
     * @param ResourceType $resourceType    The type of the identifier in the segment
441
     *
442
     * @throws ODataException      If validation fails
443
     * @throws ReflectionException
444
     */
445
    public function validate(string $segmentAsString, ResourceType $resourceType): void
446
    {
447
        if ($this->isEmpty()) {
448
            $this->validatedNamedValues = [];
449
            return;
450
        }
451
452
        $keyProperties      = $resourceType->getKeyProperties();
453
        $keyPropertiesCount = count($keyProperties);
454
        if (!empty($this->namedValues)) {
455
            if (count($this->namedValues) != $keyPropertiesCount) {
456
                throw ODataException::createSyntaxError(
457
                    Messages::keyDescriptorKeyCountNotMatching(
458
                        $segmentAsString,
459
                        $keyPropertiesCount,
460
                        count($this->namedValues)
461
                    )
462
                );
463
            }
464
465
            foreach ($keyProperties as $keyName => $keyResourceProperty) {
466
                if (!array_key_exists($keyName, $this->namedValues)) {
467
                    $keysAsString = null;
468
                    foreach (array_keys($keyProperties) as $key) {
469
                        $keysAsString .= $key . ', ';
470
                    }
471
472
                    $keysAsString = rtrim($keysAsString, ' ,');
473
                    throw ODataException::createSyntaxError(
474
                        Messages::keyDescriptorMissingKeys(
475
                            $segmentAsString,
476
                            $keysAsString
477
                        )
478
                    );
479
                }
480
481
                /** @var IType $typeProvided */
482
                $typeProvided = $this->namedValues[$keyName][1];
483
                $expectedType = $keyResourceProperty->getInstanceType();
484
                assert($expectedType instanceof IType, get_class($expectedType));
485
                if (!$expectedType->isCompatibleWith($typeProvided)) {
486
                    throw ODataException::createSyntaxError(
487
                        Messages::keyDescriptorInCompatibleKeyType(
488
                            $segmentAsString,
489
                            $keyName,
490
                            $expectedType->getFullTypeName(),
491
                            $typeProvided->getFullTypeName()
492
                        )
493
                    );
494
                }
495
496
                $this->validatedNamedValues[$keyName] = $this->namedValues[$keyName];
497
            }
498
        } else {
499
            $numPos = count($this->positionalValues);
500
            if ($numPos != $keyPropertiesCount) {
501
                throw ODataException::createSyntaxError(
502
                    Messages::keyDescriptorKeyCountNotMatching($segmentAsString, $keyPropertiesCount, $numPos)
503
                );
504
            }
505
506
            $i = 0;
507
            foreach ($keyProperties as $keyName => $keyResourceProperty) {
508
                /** @var IType $typeProvided */
509
                $typeProvided = $this->positionalValues[$i][1];
510
                $expectedType = $keyResourceProperty->getInstanceType();
511
                assert($expectedType instanceof IType, get_class($expectedType));
512
513
                if (!$expectedType->isCompatibleWith($typeProvided)) {
514
                    throw ODataException::createSyntaxError(
515
                        Messages::keyDescriptorInCompatibleKeyTypeAtPosition(
516
                            $segmentAsString,
517
                            $keyResourceProperty->getName(),
518
                            $i,
519
                            $expectedType->getFullTypeName(),
520
                            $typeProvided->getFullTypeName()
521
                        )
522
                    );
523
                }
524
525
                $this->validatedNamedValues[$keyName] = $this->positionalValues[$i];
526
                ++$i;
527
            }
528
        }
529
    }
530
531
    /**
532
     * Generate relative edit url for this key descriptor and supplied resource set.
533
     *
534
     * @param ResourceSet $resourceSet
535
     *
536
     * @throws ReflectionException
537
     * @throws InvalidArgumentException
538
     * @return string
539
     */
540
    public function generateRelativeUri(ResourceSet $resourceSet): string
541
    {
542
        $resourceType = $resourceSet->getResourceType();
543
        $keys         = $resourceType->getKeyProperties();
544
545
        $namedKeys = $this->getNamedValues();
546
        $keys = array_intersect_key($keys, $namedKeys);
547
        if (0 == count($keys) || count($keys) !== count($namedKeys)) {
548
            $msg = 'Mismatch between supplied key predicates and keys defined on resource set';
549
            throw new InvalidArgumentException($msg);
550
        }
551
552
        $editUrl = $resourceSet->getName() . '(';
553
        $comma   = null;
554
        foreach ($keys as $keyName => $resourceProperty) {
555
            $keyType = $resourceProperty->getInstanceType();
556
            assert($keyType instanceof IType, '$keyType not instanceof IType');
557
            $keyValue = $namedKeys[$keyName][0];
558
            $keyValue = $keyType->convertToOData($keyValue);
559
560
            $editUrl .= $comma . $keyName . '=' . $keyValue;
561
            $comma = ',';
562
        }
563
564
        $editUrl .= ')';
565
566
        return $editUrl;
567
    }
568
569
    /**
570
     * Gets collection of named key values.
571
     *
572
     * @return array[]
573
     */
574
    public function getNamedValues(): array
575
    {
576
        return $this->namedValues;
577
    }
578
579
    /**
580
     * Convert validated named values into an array of ODataProperties.
581
     *
582
     * return array[]
583
     * @throws InvalidOperationException
584
     */
585
    public function getODataProperties(): array
586
    {
587
        $values = $this->getValidatedNamedValues();
588
        $result = [];
589
590
        foreach ($values as $propName => $propDeets) {
591
            assert(2 == count($propDeets));
592
            assert($propDeets[1] instanceof IType);
593
            $property           = new ODataProperty();
594
            $property->name     = strval($propName);
595
            $property->value    = $propDeets[1]->convert($propDeets[0]);
596
            $property->typeName = $propDeets[1]->getFullTypeName();
597
            $result[$propName]  = $property;
598
        }
599
600
        return $result;
601
    }
602
603
    /**
604
     * Gets validated named key values, this array will be populated
605
     * in validate function.
606
     *
607
     * @throws InvalidOperationException If this function invoked before invoking validate function
608
     * @return array[]
609
     */
610
    public function getValidatedNamedValues(): array
611
    {
612
        if (empty($this->validatedNamedValues)) {
613
            throw new InvalidOperationException(
614
                Messages::keyDescriptorValidateNotCalled()
615
            );
616
        }
617
618
        return $this->validatedNamedValues;
619
    }
620
}
621