Passed
Pull Request — master (#265)
by Alex
03:42
created

KeyDescriptor::__construct()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 4
nop 2
dl 0
loc 19
rs 9.5222
c 0
b 0
f 0
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::getNextLexerToken($expressionLexer);
221
                if ($currentToken->getId() != ExpressionTokenId::EQUAL()) {
222
                    return false;
223
                }
224
225
                $currentToken = self::getNextLexerToken($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::getNextLexerToken($expressionLexer);
274
            if ($currentToken->getId() == ExpressionTokenId::COMMA()) {
275
                $currentToken = self::getNextLexerToken($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 getNextLexerToken(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
        if ($this->isEmpty()) {
420
            return 0;
421
        }
422
        if (!empty($this->namedValues)) {
423
            return count($this->namedValues);
424
        }
425
426
        return count($this->positionalValues);
427
    }
428
429
    /**
430
     * Check whether this KeyDescription has any key values.
431
     *
432
     * @return bool
433
     */
434
    public function isEmpty(): bool
435
    {
436
        return empty($this->namedValues) && empty($this->positionalValues);
437
    }
438
439
    /**
440
     * Validate this KeyDescriptor, If valid, this function populates
441
     * _validatedNamedValues array with key as keyName and value as an array of
442
     * key value and key type.
443
     *
444
     * @param string       $segmentAsString The segment in the form identifier
445
     *                                      (keyPredicate) which this descriptor
446
     *                                      represents
447
     * @param ResourceType $resourceType    The type of the identifier in the segment
448
     *
449
     * @throws ODataException      If validation fails
450
     * @throws ReflectionException
451
     */
452
    public function validate(string $segmentAsString, ResourceType $resourceType): void
453
    {
454
        if ($this->isEmpty()) {
455
            $this->validatedNamedValues = [];
456
            return;
457
        }
458
459
        $keyProperties      = $resourceType->getKeyProperties();
460
        $keyPropertiesCount = count($keyProperties);
461
        if (!empty($this->namedValues)) {
462
            if (count($this->namedValues) != $keyPropertiesCount) {
463
                throw ODataException::createSyntaxError(
464
                    Messages::keyDescriptorKeyCountNotMatching(
465
                        $segmentAsString,
466
                        $keyPropertiesCount,
467
                        count($this->namedValues)
468
                    )
469
                );
470
            }
471
472
            foreach ($keyProperties as $keyName => $keyResourceProperty) {
473
                if (!array_key_exists($keyName, $this->namedValues)) {
474
                    $keysAsString = null;
475
                    foreach (array_keys($keyProperties) as $key) {
476
                        $keysAsString .= $key . ', ';
477
                    }
478
479
                    $keysAsString = rtrim($keysAsString, ' ,');
480
                    throw ODataException::createSyntaxError(
481
                        Messages::keyDescriptorMissingKeys(
482
                            $segmentAsString,
483
                            $keysAsString
484
                        )
485
                    );
486
                }
487
488
                /** @var IType $typeProvided */
489
                $typeProvided = $this->namedValues[$keyName][1];
490
                $expectedType = $keyResourceProperty->getInstanceType();
491
                assert($expectedType instanceof IType, get_class($expectedType));
492
                if (!$expectedType->isCompatibleWith($typeProvided)) {
493
                    throw ODataException::createSyntaxError(
494
                        Messages::keyDescriptorInCompatibleKeyType(
495
                            $segmentAsString,
496
                            $keyName,
497
                            $expectedType->getFullTypeName(),
498
                            $typeProvided->getFullTypeName()
499
                        )
500
                    );
501
                }
502
503
                $this->validatedNamedValues[$keyName] = $this->namedValues[$keyName];
504
            }
505
        } else {
506
            if (count($this->positionalValues) != $keyPropertiesCount) {
507
                throw ODataException::createSyntaxError(
508
                    Messages::keyDescriptorKeyCountNotMatching(
509
                        $segmentAsString,
510
                        $keyPropertiesCount,
511
                        count($this->positionalValues)
512
                    )
513
                );
514
            }
515
516
            $i = 0;
517
            foreach ($keyProperties as $keyName => $keyResourceProperty) {
518
                /** @var IType $typeProvided */
519
                $typeProvided = $this->positionalValues[$i][1];
520
                $expectedType = $keyResourceProperty->getInstanceType();
521
                assert($expectedType instanceof IType, get_class($expectedType));
522
523
                if (!$expectedType->isCompatibleWith($typeProvided)) {
524
                    throw ODataException::createSyntaxError(
525
                        Messages::keyDescriptorInCompatibleKeyTypeAtPosition(
526
                            $segmentAsString,
527
                            $keyResourceProperty->getName(),
528
                            $i,
529
                            $expectedType->getFullTypeName(),
530
                            $typeProvided->getFullTypeName()
531
                        )
532
                    );
533
                }
534
535
                $this->validatedNamedValues[$keyName] = $this->positionalValues[$i];
536
                ++$i;
537
            }
538
        }
539
    }
540
541
    /**
542
     * Generate relative edit url for this key descriptor and supplied resource set.
543
     *
544
     * @param ResourceSet $resourceSet
545
     *
546
     * @throws ReflectionException
547
     * @throws InvalidArgumentException
548
     * @return string
549
     */
550
    public function generateRelativeUri(ResourceSet $resourceSet): string
551
    {
552
        $resourceType = $resourceSet->getResourceType();
553
        $keys         = $resourceType->getKeyProperties();
554
555
        $namedKeys = $this->getNamedValues();
556
        assert(0 !== count($keys), 'count($keys) == 0');
557
        if (count($keys) !== count($namedKeys)) {
558
            $msg = 'Mismatch between supplied key predicates and number of keys defined on resource set';
559
            throw new InvalidArgumentException($msg);
560
        }
561
        $editUrl = $resourceSet->getName() . '(';
562
        $comma   = null;
563
        foreach ($keys as $keyName => $resourceProperty) {
564
            if (!array_key_exists($keyName, $namedKeys)) {
565
                $msg = 'Key predicate ' . $keyName . ' not present in named values';
566
                throw new InvalidArgumentException($msg);
567
            }
568
            $keyType = $resourceProperty->getInstanceType();
569
            assert($keyType instanceof IType, '$keyType not instanceof IType');
570
            $keyValue = $namedKeys[$keyName][0];
571
            $keyValue = $keyType->convertToOData($keyValue);
572
573
            $editUrl .= $comma . $keyName . '=' . $keyValue;
574
            $comma = ',';
575
        }
576
577
        $editUrl .= ')';
578
579
        return $editUrl;
580
    }
581
582
    /**
583
     * Gets collection of named key values.
584
     *
585
     * @return array[]
586
     */
587
    public function getNamedValues(): array
588
    {
589
        return $this->namedValues;
590
    }
591
592
    /**
593
     * Convert validated named values into an array of ODataProperties.
594
     *
595
     * return array[]
596
     * @throws InvalidOperationException
597
     */
598
    public function getODataProperties(): array
599
    {
600
        $values = $this->getValidatedNamedValues();
601
        $result = [];
602
603
        foreach ($values as $propName => $propDeets) {
604
            assert(2 == count($propDeets));
605
            assert($propDeets[1] instanceof IType);
606
            $property           = new ODataProperty();
607
            $property->name     = strval($propName);
608
            $property->value    = $propDeets[1]->convert($propDeets[0]);
609
            $property->typeName = $propDeets[1]->getFullTypeName();
610
            $result[$propName]  = $property;
611
        }
612
613
        return $result;
614
    }
615
616
    /**
617
     * Gets validated named key values, this array will be populated
618
     * in validate function.
619
     *
620
     * @throws InvalidOperationException If this function invoked before invoking validate function
621
     * @return array[]
622
     */
623
    public function getValidatedNamedValues(): array
624
    {
625
        if (empty($this->validatedNamedValues)) {
626
            throw new InvalidOperationException(
627
                Messages::keyDescriptorValidateNotCalled()
628
            );
629
        }
630
631
        return $this->validatedNamedValues;
632
    }
633
}
634