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

UriProcessorNew::getModelDeserialiser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace POData\UriProcessor;
4
5
use POData\Common\HttpStatus;
6
use POData\Common\Messages;
7
use POData\Common\ODataConstants;
8
use POData\Common\ODataException;
9
use POData\IService;
10
use POData\ObjectModel\CynicDeserialiser;
11
use POData\ObjectModel\ModelDeserialiser;
12
use POData\ObjectModel\ODataEntry;
13
use POData\OperationContext\HTTPRequestMethod;
14
use POData\Providers\Metadata\ResourcePropertyKind;
15
use POData\Providers\Metadata\ResourceSet;
16
use POData\Providers\Metadata\ResourceSetWrapper;
17
use POData\Providers\ProvidersWrapper;
18
use POData\Providers\Query\QueryResult;
19
use POData\Providers\Query\QueryType;
20
use POData\UriProcessor\Interfaces\IUriProcessor;
21
use POData\UriProcessor\QueryProcessor\QueryProcessor;
22
use POData\UriProcessor\ResourcePathProcessor\ResourcePathProcessor;
23
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
24
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\SegmentDescriptor;
25
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
26
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetSource;
27
28
/**
29
 * Class UriProcessorNew.
30
 *
31
 * A type to process client's requested URI
32
 * The syntax of request URI is:
33
 *  Scheme Host Port ServiceRoot ResourcePath ? QueryOption
34
 * For more details refer:
35
 * http://www.odata.org/developers/protocols/uri-conventions#UriComponents
36
 */
37
class UriProcessorNew implements IUriProcessor
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...
38
{
39
    /**
40
     * Description of the OData request that a client has submitted.
41
     *
42
     * @var RequestDescription
43
     */
44
    private $request;
45
46
    /**
47
     * Holds reference to the data service instance.
48
     *
49
     * @var IService
50
     */
51
    private $service;
52
53
    /**
54
     * Holds reference to the wrapper over IDSMP and IDSQP implementation.
55
     *
56
     * @var ProvidersWrapper
57
     */
58
    private $providers;
59
60
    /**
61
     * Holds reference to request expander.
62
     *
63
     * @var RequestExpander
64
     */
65
    private $expander;
66
67
    /**
68
     * @var ModelDeserialiser
69
     */
70
    private $cereal;
71
72
    /**
73
     * @var CynicDeserialiser
74
     */
75
    private $cynicDeserialiser;
76
77
    /**
78
     * Constructs a new instance of UriProcessor.
79
     *
80
     * @param IService $service Reference to the data service instance
81
     */
82
    private function __construct(IService $service)
83
    {
84
        $this->service = $service;
85
        $this->providers = $service->getProvidersWrapper();
86
        $this->request = ResourcePathProcessor::process($service);
87
        $this->expander = new RequestExpander(
88
            $this->getRequest(),
89
            $this->getService(),
90
            $this->getProviders()
91
        );
92
        $this->getRequest()->setUriProcessor($this);
93
        $this->cereal = new ModelDeserialiser();
94
        $this->cynicDeserialiser = new CynicDeserialiser(
95
            $service->getMetadataProvider(),
96
            $service->getProvidersWrapper()
97
        );
98
    }
99
100
    /**
101
     * Process the resource path and query options of client's request uri.
102
     *
103
     * @param IService $service Reference to the data service instance
104
     *
105
     * @throws ODataException
106
     *
107
     * @return IUriProcessor
108
     */
109
    public static function process(IService $service)
110
    {
111
        $absRequestUri = $service->getHost()->getAbsoluteRequestUri();
112
        $absServiceUri = $service->getHost()->getAbsoluteServiceUri();
113
114
        if (!$absServiceUri->isBaseOf($absRequestUri)) {
115
            throw ODataException::createInternalServerError(
116
                Messages::uriProcessorRequestUriDoesNotHaveTheRightBaseUri(
117
                    $absRequestUri->getUrlAsString(),
118
                    $absServiceUri->getUrlAsString()
119
                )
120
            );
121
        }
122
123
        $processor = new self($service);
124
125
        //Parse the query string options of the request Uri.
126
        QueryProcessor::process($processor->request, $service);
127
128
        return $processor;
129
    }
130
131
    /**
132
     * Gets reference to the request submitted by client.
133
     *
134
     * @return RequestDescription
135
     */
136
    public function getRequest()
137
    {
138
        return $this->request;
139
    }
140
141
    /**
142
     * Gets reference to the request submitted by client.
143
     *
144
     * @return ProvidersWrapper
145
     */
146
    public function getProviders()
147
    {
148
        return $this->providers;
149
    }
150
151
    /**
152
     * Gets the data service instance.
153
     *
154
     * @return IService
155
     */
156
    public function getService()
157
    {
158
        return $this->service;
159
    }
160
161
    /**
162
     * Gets the request expander instance.
163
     *
164
     * @return RequestExpander
165
     */
166
    public function getExpander()
167
    {
168
        return $this->expander;
169
    }
170
171
    /**
172
     * @return ModelDeserialiser
173
     */
174
    public function getModelDeserialiser()
175
    {
176
        return $this->cereal;
177
    }
178
179
    public function getCynicDeserialiser()
180
    {
181
        return $this->cynicDeserialiser;
182
    }
183
184
    /**
185
     * Execute the client submitted request against the data source.
186
     */
187
    public function execute()
188
    {
189
        $service = $this->getService();
190
        assert($service instanceof IService, '!($service instanceof IService)');
191
        $context = $service->getOperationContext();
192
        $method = $context->incomingRequest()->getMethod();
193
194
        switch ($method) {
195
            case HTTPRequestMethod::GET():
196
                $this->executeGet();
197
                break;
198
            case HTTPRequestMethod::DELETE():
199
                $this->executeGet();
200
                $this->executeDelete();
201
                break;
202
            case HTTPRequestMethod::PUT():
203
                $this->executeGet();
204
                $this->executePut();
205
                break;
206
            case HTTPRequestMethod::POST():
207
                $this->executePost();
208
                break;
209
            default:
210
                throw ODataException::createNotImplementedError(Messages::onlyReadSupport($method));
211
        }
212
213
        // Apply $select and $expand options to result set, this function will be always applied
214
        // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
215
        // not delegate $expand/$select operation to IDSQP2 implementation
216
        $this->getExpander()->handleExpansion();
217
    }
218
219
    /**
220
     * Execute the client submitted request against the data source (GET).
221
     */
222
    protected function executeGet()
223
    {
224
        $segments = $this->getRequest()->getSegments();
225
226
        foreach ($segments as $segment) {
227
            $requestTargetKind = $segment->getTargetKind();
228
229
            switch ($requestTargetKind) {
230
                case TargetKind::SINGLETON():
231
                    $this->executeGetSingleton($segment);
232
                    break;
233
                case TargetKind::RESOURCE():
234
                    $this->executeGetResource($segment);
235
                    break;
236
                case TargetKind::MEDIA_RESOURCE():
237
                    $this->checkResourceExistsByIdentifier($segment);
238
                    $segment->setResult($segment->getPrevious()->getResult());
239
                    // a media resource means we're done - bail out of segment processing
240
                    break 2;
241
                case TargetKind::LINK():
242
                    $this->executeGetLink($segment);
243
                    break;
244
                case TargetKind::PRIMITIVE_VALUE():
245
                    $previous = $segment->getPrevious();
246
                    if (null !== $previous && TargetKind::RESOURCE() == $previous->getTargetKind()) {
247
                        $result = $previous->getResult();
248
                        if ($result instanceof QueryResult) {
249
                            $raw = null !== $result->count ? $result->count : count($result->results);
250
                            $segment->setResult($raw);
251
                        }
252
                    }
253
                    break;
254
                case TargetKind::PRIMITIVE():
255
                case TargetKind::COMPLEX_OBJECT():
256
                case TargetKind::BAG():
257
                    break;
258
                default:
259
                    assert(false, 'Not implemented yet');
260
            }
261
262
            if (null === $segment->getNext()
263
                || ODataConstants::URI_COUNT_SEGMENT == $segment->getNext()->getIdentifier()
264
            ) {
265
                $this->applyQueryOptions($segment);
266
            }
267
        }
268
    }
269
270
    /**
271
     * Execute the client submitted request against the data source (DELETE).
272
     */
273
    protected function executeDelete()
274
    {
275
        $segment = $this->getFinalEffectiveSegment();
276
        $requestMethod = $this->getService()->getOperationContext()->incomingRequest()->getMethod();
277
        $resourceSet = $segment->getTargetResourceSetWrapper();
278
        $keyDescriptor = $segment->getKeyDescriptor();
279
280
        $this->checkUriValidForSuppliedVerb($resourceSet, $keyDescriptor, $requestMethod);
281
        assert($resourceSet instanceof ResourceSet);
282
        $this->getProviders()->deleteResource($resourceSet, $segment->getResult());
283
        $this->getService()->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT);
284
    }
285
286
    /**
287
     * Execute the client submitted request against the data source (PUT).
288
     */
289
    protected function executePut()
290
    {
291
        $segment = $this->getFinalEffectiveSegment();
292
        $requestMethod = $this->getService()->getOperationContext()->incomingRequest()->getMethod();
293
        $resourceSet = null !== $segment ? $segment->getTargetResourceSetWrapper() : null;
294
        $keyDescriptor = null !== $segment ? $segment->getKeyDescriptor() : null;
295
296
        $this->checkUriValidForSuppliedVerb($resourceSet, $keyDescriptor, $requestMethod);
297
        assert($resourceSet instanceof ResourceSet);
298
        assert($keyDescriptor instanceof KeyDescriptor);
299
300
        $payload = $this->getRequest()->getData();
301
        assert($payload instanceof ODataEntry, get_class($payload));
302
        assert(!empty($payload->id), 'Payload ID must not be empty for PUT request');
303
        $data = $this->getModelDeserialiser()->bulkDeserialise($resourceSet->getResourceType(), $payload);
304
305
        if (empty($data)) {
306
            throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
307
        }
308
309
        $queryResult = $this->getCynicDeserialiser()->processPayload($payload);
310
        $segment->setResult($queryResult);
311
    }
312
313
    /**
314
     * Execute the client submitted request against the data source (POST).
315
     */
316
    protected function executePost()
317
    {
318
        $segments = $this->getRequest()->getSegments();
319
        $requestMethod = $this->getService()->getOperationContext()->incomingRequest()->getMethod();
320
321
        foreach ($segments as $segment) {
322
            $requestTargetKind = $segment->getTargetKind();
323
            if ($requestTargetKind == TargetKind::RESOURCE()) {
324
                $resourceSet = $segment->getTargetResourceSetWrapper();
325 View Code Duplication
                if (!$resourceSet) {
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...
326
                    $url = $this->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
327
                    $msg = Messages::badRequestInvalidUriForThisVerb($url, $requestMethod);
328
                    throw ODataException::createBadRequestError($msg);
329
                }
330
331
                $payload = $this->getRequest()->getData();
332
                assert($payload instanceof ODataEntry, get_class($payload));
333
                assert(empty($payload->id), 'Payload ID must be empty for POST request');
334
                $data = $this->getModelDeserialiser()->bulkDeserialise($resourceSet->getResourceType(), $payload);
335
336
                if (empty($data)) {
337
                    throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
338
                }
339
                $this->getService()->getHost()->setResponseStatusCode(HttpStatus::CODE_CREATED);
340
                $queryResult = $this->getCynicDeserialiser()->processPayload($payload);
341
                $keyID = $payload->id;
342
                assert($keyID instanceof KeyDescriptor, get_class($keyID));
343
                $locationUrl = $keyID->generateRelativeUri($resourceSet->getResourceSet());
344
                $absoluteServiceUri = $this->getService()->getHost()->getAbsoluteServiceUri()->getUrlAsString();
345
                $location = rtrim($absoluteServiceUri, '/') . '/' . $locationUrl;
346
                $this->getService()->getHost()->setResponseLocation($location);
347
                $segment->setResult($queryResult);
348
            }
349
        }
350
    }
351
352
    /**
353
     * @return null|SegmentDescriptor
354
     */
355
    protected function getFinalEffectiveSegment()
356
    {
357
        $segment = $this->getRequest()->getLastSegment();
358
        // if last segment is $count, back up one
359
        if (null !== $segment && ODataConstants::URI_COUNT_SEGMENT == $segment->getIdentifier()) {
360
            $segment = $segment->getPrevious();
361
            return $segment;
362
        }
363
        return $segment;
364
    }
365
366
    /**
367
     * @param $resourceSet
368
     * @param $keyDescriptor
369
     * @param $requestMethod
370
     * @throws ODataException
371
     */
372
    protected function checkUriValidForSuppliedVerb($resourceSet, $keyDescriptor, $requestMethod)
373
    {
374 View Code Duplication
        if (!$resourceSet || !$keyDescriptor) {
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...
375
            $url = $this->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
376
            throw ODataException::createBadRequestError(
377
                Messages::badRequestInvalidUriForThisVerb($url, $requestMethod)
378
            );
379
        }
380
    }
381
382
    /**
383
     * @param $segment
384
     */
385
    private function executeGetSingleton($segment)
386
    {
387
        $segmentId = $segment->getIdentifier();
388
        $singleton = $this->getService()->getProvidersWrapper()->resolveSingleton($segmentId);
389
        $segment->setResult($singleton->get());
390
    }
391
392
    /**
393
     * @param $segment
394
     */
395
    private function executeGetResource($segment)
396
    {
397
        $isRelated = $segment->getTargetSource() != TargetSource::ENTITY_SET;
398
        if (!$isRelated) {
399
            $queryResult = $this->executeGetResourceDirect($segment);
400
        } else {
401
            $queryResult = $this->executeGetResourceRelated($segment);
402
        }
403
        $segment->setResult($queryResult);
404
    }
405
406
    /**
407
     * @param $segment
408
     */
409
    private function executeGetLink($segment)
410
    {
411
        $previous = $segment->getPrevious();
412
        assert(isset($previous));
413
        $segment->setResult($previous->getResult());
414
    }
415
416
    /**
417
     * @param $segment
418
     * @return null|object|QueryResult
419
     */
420
    private function executeGetResourceDirect($segment)
421
    {
422
        if ($segment->isSingleResult()) {
423
            $queryResult = $this->getProviders()->getResourceFromResourceSet(
424
                $segment->getTargetResourceSetWrapper(),
425
                $segment->getKeyDescriptor()
426
            );
427
        } else {
428
            $skip = $this->getRequest()->getSkipCount();
429
            $skip = (null === $skip) ? 0 : $skip;
430
            $skipToken = $this->getRequest()->getInternalSkipTokenInfo();
431
            $skipToken = (null != $skipToken) ? $skipToken->getSkipTokenInfo() : null;
432
            $queryResult = $this->getProviders()->getResourceSet(
433
                $this->getRequest()->queryType,
434
                $segment->getTargetResourceSetWrapper(),
435
                $this->getRequest()->getFilterInfo(),
436
                $this->getRequest()->getInternalOrderByInfo(),
437
                $this->getRequest()->getTopCount(),
438
                $skip,
439
                $skipToken
440
            );
441
        }
442
        return $queryResult;
443
    }
444
445
    /**
446
     * @param $segment
447
     * @return null|object|QueryResult
448
     */
449
    private function executeGetResourceRelated($segment)
450
    {
451
        $projectedProperty = $segment->getProjectedProperty();
452
        $projectedPropertyKind = null !== $projectedProperty ? $projectedProperty->getKind() : 0;
453
        $queryResult = null;
454
        switch ($projectedPropertyKind) {
455
            case ResourcePropertyKind::RESOURCE_REFERENCE:
456
                $queryResult = $this->getProviders()->getRelatedResourceReference(
457
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
458
                    $segment->getPrevious()->getResult(),
459
                    $segment->getTargetResourceSetWrapper(),
460
                    $projectedProperty
461
                );
462
                break;
463
            case ResourcePropertyKind::RESOURCESET_REFERENCE:
464
                if ($segment->isSingleResult()) {
465
                    $queryResult = $this->getProviders()->getResourceFromRelatedResourceSet(
466
                        $segment->getPrevious()->getTargetResourceSetWrapper(),
467
                        $segment->getPrevious()->getResult(),
468
                        $segment->getTargetResourceSetWrapper(),
469
                        $projectedProperty,
470
                        $segment->getKeyDescriptor()
471
                    );
472
                } else {
473
                    $skipToken = $this->getRequest()->getInternalSkipTokenInfo();
474
                    $skipToken = (null !== $skipToken) ? $skipToken->getSkipTokenInfo() : null;
475
                    $queryResult = $this->getProviders()->getRelatedResourceSet(
476
                        $this->getRequest()->queryType,
477
                        $segment->getPrevious()->getTargetResourceSetWrapper(),
478
                        $segment->getPrevious()->getResult(),
479
                        $segment->getTargetResourceSetWrapper(),
480
                        $projectedProperty,
481
                        $this->getRequest()->getFilterInfo(),
482
                        null, // $orderby
483
                        null, // $top
484
                        null, // $skip
485
                        $skipToken
486
                    );
487
                }
488
                break;
489
            default:
490
                $this->checkResourceExistsByIdentifier($segment);
491
                assert(false, 'Invalid property kind type for resource retrieval');
492
        }
493
        return $queryResult;
494
    }
495
496
    /**
497
     * @param $segment
498
     * @throws ODataException
499
     */
500
    private function checkResourceExistsByIdentifier($segment)
501
    {
502
        if (null === $segment->getPrevious()->getResult()) {
503
            throw ODataException::createResourceNotFoundError(
504
                $segment->getPrevious()->getIdentifier()
505
            );
506
        }
507
    }
508
509
    /**
510
     * Applies the query options to the resource(s) retrieved from the data source.
511
     *
512
     * @param SegmentDescriptor $segment The descriptor which holds resource(s) on which query options to be applied
513
     */
514
    private function applyQueryOptions(SegmentDescriptor $segment)
515
    {
516
        $result = $segment->getResult();
517
        if (!$result instanceof QueryResult) {
518
            //If the segment isn't a query result, then there's no paging or counting to be done
519
            return;
520
        }
521
        // Note $inlinecount=allpages means include the total count regardless of paging..so we set the counts first
522
        // regardless if POData does the paging or not.
523 View Code Duplication
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
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...
524
            if ($this->getProviders()->handlesOrderedPaging()) {
525
                $this->getRequest()->setCountValue($result->count);
526
            } else {
527
                $this->getRequest()->setCountValue(count($result->results));
528
            }
529
        }
530
        //Have POData perform paging if necessary
531
        if (!$this->getProviders()->handlesOrderedPaging() && !empty($result->results)) {
532
            $result->results = $this->performPaging($result->results);
533
        }
534
        //a bit surprising, but $skip and $top affects $count so update it here, not above
535
        //IE  data.svc/Collection/$count?$top=10 returns 10 even if Collection has 11+ entries
536 View Code Duplication
        if ($this->getRequest()->queryType == QueryType::COUNT()) {
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...
537
            if ($this->getProviders()->handlesOrderedPaging()) {
538
                $this->getRequest()->setCountValue($result->count);
539
            } else {
540
                $this->getRequest()->setCountValue(count($result->results));
541
            }
542
        }
543
        $segment->setResult($result);
544
    }
545
546
    /**
547
     * If the provider does not perform the paging (ordering, top, skip) then this method does it.
548
     *
549
     * @param array $result
550
     *
551
     * @return array
552
     */
553
    private function performPaging(array $result)
554
    {
555
        //Apply (implicit and explicit) $orderby option
556
        $internalOrderByInfo = $this->getRequest()->getInternalOrderByInfo();
557
        if (null !== $internalOrderByInfo) {
558
            $orderByFunction = $internalOrderByInfo->getSorterFunction();
559
            usort($result, $orderByFunction);
560
        }
561
        //Apply $skiptoken option
562
        $internalSkipTokenInfo = $this->getRequest()->getInternalSkipTokenInfo();
563
        if (null !== $internalSkipTokenInfo) {
564
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
565
            $result = array_slice($result, $matchingIndex);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $result. This often makes code more readable.
Loading history...
566
        }
567
        //Apply $top and $skip option
568
        if (!empty($result)) {
569
            $top = $this->getRequest()->getTopCount();
570
            $skip = $this->getRequest()->getSkipCount();
571
            if (null === $skip) {
572
                $skip = 0;
573
            }
574
            $result = array_slice($result, $skip, $top);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $result. This often makes code more readable.
Loading history...
575
        }
576
        return $result;
577
    }
578
}
579