Completed
Push — master ( ac6276...602f49 )
by Alex
04:45
created

CynicSerialiser::writeUrlElements()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 34
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 34
rs 6.7272
cc 7
eloc 24
nc 6
nop 1
1
<?php
2
3
namespace POData\ObjectModel;
4
5
use Carbon\Carbon;
6
use Illuminate\Support\Collection;
7
use POData\Common\InvalidOperationException;
8
use POData\Common\Messages;
9
use POData\Common\ODataConstants;
10
use POData\Common\ODataException;
11
use POData\IService;
12
use POData\Providers\Metadata\ResourceComplexType;
13
use POData\Providers\Metadata\ResourceEntityType;
14
use POData\Providers\Metadata\ResourcePrimitiveType;
15
use POData\Providers\Metadata\ResourceProperty;
16
use POData\Providers\Metadata\ResourcePropertyKind;
17
use POData\Providers\Metadata\ResourceSet;
18
use POData\Providers\Metadata\ResourceSetWrapper;
19
use POData\Providers\Metadata\ResourceType;
20
use POData\Providers\Metadata\ResourceTypeKind;
21
use POData\Providers\Metadata\Type\Binary;
22
use POData\Providers\Metadata\Type\Boolean;
23
use POData\Providers\Metadata\Type\DateTime;
24
use POData\Providers\Metadata\Type\IType;
25
use POData\Providers\Metadata\Type\StringType;
26
use POData\Providers\Query\QueryResult;
27
use POData\Providers\Query\QueryType;
28
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
29
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ProjectionNode;
30
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\RootProjectionNode;
31
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
32
use POData\UriProcessor\RequestDescription;
33
use POData\UriProcessor\SegmentStack;
34
35
class CynicSerialiser implements IObjectSerialiser
36
{
37
    /**
38
     * The service implementation.
39
     *
40
     * @var IService
41
     */
42
    protected $service;
43
44
    /**
45
     * Request description instance describes OData request the
46
     * the client has submitted and result of the request.
47
     *
48
     * @var RequestDescription
49
     */
50
    protected $request;
51
52
    /**
53
     * Collection of complex type instances used for cycle detection.
54
     *
55
     * @var array
56
     */
57
    protected $complexTypeInstanceCollection;
58
59
    /**
60
     * Absolute service Uri.
61
     *
62
     * @var string
63
     */
64
    protected $absoluteServiceUri;
65
66
    /**
67
     * Absolute service Uri with slash.
68
     *
69
     * @var string
70
     */
71
    protected $absoluteServiceUriWithSlash;
72
73
    /**
74
     * Holds reference to segment stack being processed.
75
     *
76
     * @var SegmentStack
77
     */
78
    protected $stack;
79
80
    /**
81
     * Lightweight stack tracking for recursive descent fill.
82
     */
83
    private $lightStack = [];
84
85
    /*
86
     * Update time to insert into ODataEntry/ODataFeed fields
87
     * @var Carbon;
88
     */
89
    private $updated;
90
91
    /*
92
     * Has base URI already been written out during serialisation?
93
     * @var bool;
94
     */
95
    private $isBaseWritten = false;
96
97
    /**
98
     * @param IService                $service Reference to the data service instance
99
     * @param RequestDescription|null $request Type instance describing the client submitted request
100
     */
101
    public function __construct(IService $service, RequestDescription $request = null)
102
    {
103
        $this->service = $service;
104
        $this->request = $request;
105
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
106
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
107
        $this->stack = new SegmentStack($request);
108
        $this->complexTypeInstanceCollection = [];
109
        $this->updated = Carbon::now();
110
    }
111
112
    /**
113
     * Write a top level entry resource.
114
     *
115
     * @param QueryResult $entryObject Results property contains reference to the entry object to be written
116
     *
117
     * @return ODataEntry|null
118
     */
119
    public function writeTopLevelElement(QueryResult $entryObject)
120
    {
121
        if (!isset($entryObject->results)) {
122
            array_pop($this->lightStack);
123
            return null;
124
        }
125
126
        $this->loadStackIfEmpty();
127
128
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
129
        $this->isBaseWritten = true;
130
131
        $stackCount = count($this->lightStack);
132
        $topOfStack = $this->lightStack[$stackCount-1];
133
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
134
        assert($resourceType instanceof ResourceType, get_class($resourceType));
135
        $rawProp = $resourceType->getAllProperties();
136
        $relProp = [];
137
        $nonRelProp = [];
138
        $last = end($this->lightStack);
139
        $projNodes = ($last['type'] == $last['property']) ? $this->getProjectionNodes() : null;
140
141
        foreach ($rawProp as $prop) {
142
            $propName = $prop->getName();
143
            if ($prop->getResourceType() instanceof ResourceEntityType) {
144
                $relProp[$propName] = $prop;
145
            } else {
146
                $nonRelProp[$propName] = $prop;
147
            }
148
        }
149
        $rawCount = count($rawProp);
150
        $relCount = count($relProp);
151
        $nonRelCount = count($nonRelProp);
152
        assert(
153
            $rawCount == $relCount + $nonRelCount,
154
            'Raw property count '.$rawCount.', does not equal sum of relProp count, '.$relCount
155
            .', and nonRelPropCount,'.$nonRelCount
156
        );
157
158
        // now mask off against projNodes
159
        if (null !== $projNodes) {
160
            $keys = [];
161
            foreach ($projNodes as $node) {
162
                $keys[$node->getPropertyName()] = '';
163
            }
164
165
            $relProp = array_intersect_key($relProp, $keys);
166
            $nonRelProp = array_intersect_key($nonRelProp, $keys);
167
        }
168
169
        $resourceSet = $resourceType->getCustomState();
170
        assert($resourceSet instanceof ResourceSet);
171
        $title = $resourceType->getName();
172
        $type = $resourceType->getFullName();
173
174
        $relativeUri = $this->getEntryInstanceKey(
175
            $entryObject->results,
176
            $resourceType,
177
            $resourceSet->getName()
178
        );
179
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
180
181
        list($mediaLink, $mediaLinks) = $this->writeMediaData(
182
            $entryObject->results,
183
            $type,
184
            $relativeUri,
185
            $resourceType
186
        );
187
188
        $propertyContent = $this->writeProperties($entryObject->results, $nonRelProp);
189
190
        $links = [];
191
        foreach ($relProp as $prop) {
192
            $nuLink = new ODataLink();
193
            $propKind = $prop->getKind();
194
195
            assert(
196
                ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
197
                || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind,
198
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
199
                .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
200
            );
201
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
202
            $propType = 'application/atom+xml;type='.$propTail;
203
            $propName = $prop->getName();
204
            $nuLink->title = $propName;
205
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
206
            $nuLink->url = $relativeUri . '/' . $propName;
207
            $nuLink->type = $propType;
208
209
            $navProp = new ODataNavigationPropertyInfo($prop, $this->shouldExpandSegment($propName));
210
            if ($navProp->expanded) {
211
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
212
            }
213
214
            $links[] = $nuLink;
215
        }
216
217
        $odata = new ODataEntry();
218
        $odata->resourceSetName = $resourceSet->getName();
219
        $odata->id = $absoluteUri;
220
        $odata->title = new ODataTitle($title);
221
        $odata->type = new ODataCategory($type);
222
        $odata->propertyContent = $propertyContent;
223
        $odata->isMediaLinkEntry = true === $resourceType->isMediaLinkEntry() ? true : null;
224
        $odata->editLink = new ODataLink();
225
        $odata->editLink->url = $relativeUri;
226
        $odata->editLink->name = 'edit';
227
        $odata->editLink->title = $title;
228
        $odata->mediaLink = $mediaLink;
229
        $odata->mediaLinks = $mediaLinks;
230
        $odata->links = $links;
231
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
232
        $odata->baseURI = $baseURI;
233
234
        $newCount = count($this->lightStack);
235
        assert(
236
            $newCount == $stackCount,
237
            'Should have ' . $stackCount . 'elements in stack, have ' . $newCount . 'elements'
238
        );
239
        $this->lightStack[$newCount-1]['count']--;
240
        if (0 == $this->lightStack[$newCount-1]['count']) {
241
            array_pop($this->lightStack);
242
        }
243
        return $odata;
244
    }
245
246
    /**
247
     * Write top level feed element.
248
     *
249
     * @param QueryResult &$entryObjects Results property contains array of entry resources to be written
250
     *
251
     * @return ODataFeed
252
     */
253
    public function writeTopLevelElements(QueryResult &$entryObjects)
254
    {
255
        $res = $entryObjects->results;
256
        assert(is_array($res) || $res instanceof Collection, '!is_array($entryObjects->results)');
257
258
        $this->loadStackIfEmpty();
259
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
260
261
        $title = $this->getRequest()->getContainerName();
262
        $relativeUri = $this->getRequest()->getIdentifier();
263
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
264
265
        $selfLink = new ODataLink();
266
        $selfLink->name = 'self';
267
        $selfLink->title = $title;
268
        $selfLink->url = $relativeUri;
269
270
        $odata = new ODataFeed();
271
        $odata->title = new ODataTitle($title);
272
        $odata->id = $absoluteUri;
273
        $odata->selfLink = $selfLink;
274
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
275
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
276
        $this->isBaseWritten = true;
277
278
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
279
            $odata->rowCount = $this->getRequest()->getCountValue();
280
        }
281
        foreach ($res as $entry) {
282
            if (!$entry instanceof QueryResult) {
283
                $query = new QueryResult();
284
                $query->results = $entry;
285
            } else {
286
                $query = $entry;
287
            }
288
            $odata->entries[] = $this->writeTopLevelElement($query);
289
        }
290
291
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
292
        $requestTop = $this->getRequest()->getTopOptionCount();
293
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
294
        $requestTop = (null === $requestTop) ? $pageSize + 1 : $requestTop;
295
296
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
297
            $stackSegment = $setName;
298
            $lastObject = end($entryObjects->results);
299
            $segment = $this->getNextLinkUri($lastObject);
300
            $nextLink = new ODataLink();
301
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
302
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
303
            $odata->nextPageLink = $nextLink;
304
        }
305
306
        return $odata;
307
    }
308
309
    /**
310
     * Write top level url element.
311
     *
312
     * @param QueryResult $entryObject Results property contains the entry resource whose url to be written
313
     *
314
     * @return ODataURL
315
     */
316
    public function writeUrlElement(QueryResult $entryObject)
317
    {
318
        $url = new ODataURL();
319
        if (null !== $entryObject->results) {
320
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
321
            $relativeUri = $this->getEntryInstanceKey(
322
                $entryObject->results,
323
                $currentResourceType,
324
                $this->getCurrentResourceSetWrapper()->getName()
325
            );
326
327
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
328
        }
329
330
        return $url;
331
    }
332
333
    /**
334
     * Write top level url collection.
335
     *
336
     * @param QueryResult $entryObjects Results property contains the array of entry resources whose urls are
337
     *                                  to be written
338
     *
339
     * @return ODataURLCollection
340
     */
341
    public function writeUrlElements(QueryResult $entryObjects)
342
    {
343
        $urls = new ODataURLCollection();
344
        if (!empty($entryObjects->results)) {
345
            $i = 0;
346
            foreach ($entryObjects->results as $entryObject) {
347
                if (!$entryObject instanceof QueryResult) {
348
                    $query = new QueryResult();
349
                    $query->results = $entryObject;
350
                } else {
351
                    $query = $entryObject;
352
                }
353
                $urls->urls[$i] = $this->writeUrlElement($query);
354
                ++$i;
355
            }
356
357
            if ($i > 0 && true === $entryObjects->hasMore) {
358
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
359
                $lastObject = end($entryObjects->results);
360
                $segment = $this->getNextLinkUri($lastObject);
361
                $nextLink = new ODataLink();
362
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
363
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
364
                $nextLink->url = ltrim($nextLink->url, '/');
365
                $urls->nextPageLink = $nextLink;
366
            }
367
        }
368
369
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
370
            $urls->count = $this->getRequest()->getCountValue();
371
        }
372
373
        return $urls;
374
    }
375
376
    /**
377
     * Write top level complex resource.
378
     *
379
     * @param QueryResult  &$complexValue Results property contains the complex object to be written
380
     * @param string       $propertyName  The name of the complex property
381
     * @param ResourceType &$resourceType Describes the type of complex object
382
     *
383
     * @return ODataPropertyContent
384
     */
385
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
386
    {
387
        $result = $complexValue->results;
388
389
        $propertyContent = new ODataPropertyContent();
390
        $odataProperty = new ODataProperty();
391
        $odataProperty->name = $propertyName;
392
        $odataProperty->typeName = $resourceType->getFullName();
393
        if (null !== $result) {
394
            assert(is_object($result), 'Supplied $customObject must be an object');
395
            $internalContent = $this->writeComplexValue($resourceType, $result);
396
            $odataProperty->value = $internalContent;
397
        }
398
399
        $propertyContent->properties[$propertyName] = $odataProperty;
400
401
        return $propertyContent;
402
    }
403
404
    /**
405
     * Write top level bag resource.
406
     *
407
     * @param QueryResult  $bagValue
408
     * @param string       $propertyName  The name of the bag property
409
     * @param ResourceType &$resourceType Describes the type of bag object
410
     *
411
     * @return ODataPropertyContent
412
     */
413
    public function writeTopLevelBagObject(QueryResult &$bagValue, $propertyName, ResourceType &$resourceType)
414
    {
415
        $result = $bagValue->results;
416
417
        $propertyContent = new ODataPropertyContent();
418
        $odataProperty = new ODataProperty();
419
        $odataProperty->name = $propertyName;
420
        $odataProperty->typeName = 'Collection('.$resourceType->getFullName().')';
421
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
422
423
        $propertyContent->properties[$propertyName] = $odataProperty;
424
        return $propertyContent;
425
    }
426
427
    /**
428
     * Write top level primitive value.
429
     *
430
     * @param QueryResult      &$primitiveValue   Results property contains the primitive value to be written
431
     * @param ResourceProperty &$resourceProperty Resource property describing the primitive property to be written
432
     *
433
     * @return ODataPropertyContent
434
     */
435
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
436
    {
437
        assert(null !== $resourceProperty, 'Resource property must not be null');
438
        $result = new ODataPropertyContent();
439
        $property = new ODataProperty();
440
        $property->name = $resourceProperty->getName();
441
442
        $iType = $resourceProperty->getInstanceType();
443
        assert($iType instanceof IType, get_class($iType));
444
        $property->typeName = $iType->getFullTypeName();
445
        if (null !== $primitiveValue->results) {
446
            $rType = $resourceProperty->getResourceType()->getInstanceType();
447
            assert($rType instanceof IType, get_class($rType));
448
            $property->value = $this->primitiveToString($rType, $primitiveValue->results);
449
        }
450
451
        $result->properties[$property->name] = $property;
452
        return $result;
453
    }
454
455
    /**
456
     * Gets reference to the request submitted by client.
457
     *
458
     * @return RequestDescription
459
     */
460
    public function getRequest()
461
    {
462
        assert(null !== $this->request, 'Request not yet set');
463
464
        return $this->request;
465
    }
466
467
    /**
468
     * Sets reference to the request submitted by client.
469
     *
470
     * @param  RequestDescription $request
471
     * @return void
472
     */
473
    public function setRequest(RequestDescription $request)
474
    {
475
        $this->request = $request;
476
        $this->stack->setRequest($request);
477
    }
478
479
    /**
480
     * Gets the data service instance.
481
     *
482
     * @return IService
483
     */
484
    public function getService()
485
    {
486
        return $this->service;
487
    }
488
489
    /**
490
     * Sets the data service instance.
491
     *
492
     * @param  IService $service
493
     * @return void
494
     */
495
    public function setService(IService $service)
496
    {
497
        $this->service = $service;
498
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
499
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
500
    }
501
502
    /**
503
     * Gets the segment stack instance.
504
     *
505
     * @return SegmentStack
506
     */
507
    public function getStack()
508
    {
509
        return $this->stack;
510
    }
511
512
    /**
513
     * Get update timestamp.
514
     *
515
     * @return Carbon
516
     */
517
    public function getUpdated()
518
    {
519
        return $this->updated;
520
    }
521
522
    /**
523
     * @param ResourceType $resourceType
524
     * @param $result
525
     * @return ODataBagContent|null
526
     */
527
    protected function writeBagValue(ResourceType &$resourceType, $result)
528
    {
529
        assert(null === $result || is_array($result), 'Bag parameter must be null or array');
530
        $typeKind = $resourceType->getResourceTypeKind();
531
        assert(
532
            ResourceTypeKind::PRIMITIVE() == $typeKind || ResourceTypeKind::COMPLEX() == $typeKind,
533
            '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
534
            .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX'
535
        );
536
        if (null == $result) {
537
            return null;
538
        }
539
        $bag = new ODataBagContent();
540
        foreach ($result as $value) {
541
            if (isset($value)) {
542
                if (ResourceTypeKind::PRIMITIVE() == $typeKind) {
543
                    $instance = $resourceType->getInstanceType();
544
                    assert($instance instanceof IType, get_class($instance));
545
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
546
                } elseif (ResourceTypeKind::COMPLEX() == $typeKind) {
547
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
548
                }
549
            }
550
        }
551
        return $bag;
552
    }
553
554
    /**
555
     * @param  ResourceType         $resourceType
556
     * @param  object               $result
557
     * @param  string|null          $propertyName
558
     * @return ODataPropertyContent
559
     */
560
    protected function writeComplexValue(ResourceType &$resourceType, &$result, $propertyName = null)
561
    {
562
        assert(is_object($result), 'Supplied $customObject must be an object');
563
564
        $count = count($this->complexTypeInstanceCollection);
565
        for ($i = 0; $i < $count; ++$i) {
566
            if ($this->complexTypeInstanceCollection[$i] === $result) {
567
                throw new InvalidOperationException(
568
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
569
                );
570
            }
571
        }
572
573
        $this->complexTypeInstanceCollection[$count] = &$result;
574
575
        $internalContent = new ODataPropertyContent();
576
        $resourceProperties = $resourceType->getAllProperties();
577
        // first up, handle primitive properties
578
        foreach ($resourceProperties as $prop) {
579
            $resourceKind = $prop->getKind();
580
            $propName = $prop->getName();
581
            $internalProperty = new ODataProperty();
582
            $internalProperty->name = $propName;
583
            $raw = $result->$propName;
584
            if (static::isMatchPrimitive($resourceKind)) {
585
                $iType = $prop->getInstanceType();
586
                assert($iType instanceof IType, get_class($iType));
587
                $internalProperty->typeName = $iType->getFullTypeName();
588
589
                $rType = $prop->getResourceType()->getInstanceType();
590
                assert($rType instanceof IType, get_class($rType));
591
                if (null !== $raw) {
592
                    $internalProperty->value = $this->primitiveToString($rType, $raw);
593
                }
594
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
595
                $rType = $prop->getResourceType();
596
                $internalProperty->typeName = $rType->getFullName();
597
                if (null !== $raw) {
598
                    $internalProperty->value = $this->writeComplexValue($rType, $raw, $propName);
599
                }
600
            }
601
            $internalContent->properties[$propName] = $internalProperty;
602
        }
603
604
        unset($this->complexTypeInstanceCollection[$count]);
605
        return $internalContent;
606
    }
607
608
    /**
609
     * Check whether to expand a navigation property or not.
610
     *
611
     * @param string $navigationPropertyName Name of naviagtion property in question
612
     *
613
     * @return bool True if the given navigation should be expanded, otherwise false
614
     */
615
    protected function shouldExpandSegment($navigationPropertyName)
616
    {
617
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
618
        if (null === $expandedProjectionNode) {
619
            return false;
620
        }
621
622
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
623
624
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
625
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
626
    }
627
628
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
629
    {
630
        $typeName = $resourceType->getName();
631
        $keyProperties = $resourceType->getKeyProperties();
632
        assert(0 != count($keyProperties), 'count($keyProperties) == 0');
633
        $keyString = $containerName . '(';
634
        $comma = null;
635
        foreach ($keyProperties as $keyName => $resourceProperty) {
636
            $keyType = $resourceProperty->getInstanceType();
637
            assert($keyType instanceof IType, '$keyType not instanceof IType');
638
            $keyName = $resourceProperty->getName();
639
            $keyValue = $entityInstance->$keyName;
640
            if (!isset($keyValue)) {
641
                $msg = Messages::badQueryNullKeysAreNotSupported($typeName, $keyName);
642
                throw ODataException::createInternalServerError($msg);
643
            }
644
645
            $keyValue = $keyType->convertToOData($keyValue);
646
            $keyString .= $comma . $keyName . '=' . $keyValue;
647
            $comma = ',';
648
        }
649
650
        $keyString .= ')';
651
652
        return $keyString;
653
    }
654
655
    /**
656
     * Resource set wrapper for the resource being serialized.
657
     *
658
     * @return ResourceSetWrapper
659
     */
660 View Code Duplication
    protected function getCurrentResourceSetWrapper()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
661
    {
662
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
663
        $count = count($segmentWrappers);
664
665
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count - 1];
666
    }
667
668
    /**
669
     * Wheter next link is needed for the current resource set (feed)
670
     * being serialized.
671
     *
672
     * @param int $resultSetCount Number of entries in the current
673
     *                            resource set
674
     *
675
     * @return bool true if the feed must have a next page link
676
     */
677
    protected function needNextPageLink($resultSetCount)
0 ignored issues
show
Coding Style introduced by
function needNextPageLink() 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...
678
    {
679
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
680
        $recursionLevel = count($this->getStack()->getSegmentNames());
681
        $pageSize = $currentResourceSet->getResourceSetPageSize();
682
683
        if (1 == $recursionLevel) {
684
            //presence of $top option affect next link for root container
685
            $topValueCount = $this->getRequest()->getTopOptionCount();
686
            if (null !== $topValueCount && ($topValueCount <= $pageSize)) {
687
                return false;
688
            }
689
        }
690
        return $resultSetCount == $pageSize;
691
    }
692
693
    /**
694
     * Get next page link from the given entity instance.
695
     *
696
     * @param mixed &$lastObject Last object serialized to be
697
     *                           used for generating $skiptoken
698
     *
699
     * @return string for the link for next page
700
     */
701
    protected function getNextLinkUri(&$lastObject)
702
    {
703
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
704
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
705
        assert(null !== $internalOrderByInfo);
706
        assert(is_object($internalOrderByInfo));
707
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
708
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
709
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
710
711
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
712
        assert(null !== $skipToken, '!is_null($skipToken)');
713
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
714
        $skipToken = '?'.$queryParameterString.$token.$skipToken;
715
716
        return $skipToken;
717
    }
718
719
    /**
720
     * @param $entryObject
721
     * @param $type
722
     * @param $relativeUri
723
     * @param $resourceType
724
     * @return array<ODataMediaLink|null|array>
725
     */
726
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
727
    {
728
        $context = $this->getService()->getOperationContext();
729
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
730
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
731
732
        $mediaLink = null;
733
        if ($resourceType->isMediaLinkEntry()) {
734
            $eTag = $streamProviderWrapper->getStreamETag2($entryObject, null, $context);
735
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
736
        }
737
        $mediaLinks = [];
738
        if ($resourceType->hasNamedStream()) {
739
            $namedStreams = $resourceType->getAllNamedStreams();
740
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
741
                $readUri = $streamProviderWrapper->getReadStreamUri2(
742
                    $entryObject,
743
                    $resourceStreamInfo,
744
                    $context,
745
                    $relativeUri
746
                );
747
                $mediaContentType = $streamProviderWrapper->getStreamContentType2(
748
                    $entryObject,
749
                    $resourceStreamInfo,
750
                    $context
751
                );
752
                $eTag = $streamProviderWrapper->getStreamETag2(
753
                    $entryObject,
754
                    $resourceStreamInfo,
755
                    $context
756
                );
757
758
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
759
                $mediaLinks[] = $nuLink;
760
            }
761
        }
762
        return [$mediaLink, $mediaLinks];
763
    }
764
765
    /**
766
     * @param $entryObject
767
     * @param $prop
768
     * @param $nuLink
769
     * @param $propKind
770
     * @param $propName
771
     */
772
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
773
    {
774
        $nextName = $prop->getResourceType()->getName();
775
        $nuLink->isExpanded = true;
776
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind;
777
        $nuLink->isCollection = $isCollection;
778
        $value = $entryObject->results->$propName;
779
        $nullResult = null === $value;
780
        $resultCount = $nullResult ? 0 : count($value);
781
782
        if (0 < $resultCount) {
783
            $result = new QueryResult();
784
            $result->results = $value;
785
            if (!$nullResult) {
786
                $newStackLine = ['type' => $nextName, 'property' => $propName, 'count' => $resultCount];
787
                array_push($this->lightStack, $newStackLine);
788
                if (isset($value)) {
789
                    if (!$isCollection) {
790
                        $expandedResult = $this->writeTopLevelElement($result);
791
                    } else {
792
                        $expandedResult = $this->writeTopLevelElements($result);
793
                    }
794
                    $nuLink->expandedResult = $expandedResult;
795
                }
796
            }
797
        }
798
        if (!isset($nuLink->expandedResult)) {
799
            $nuLink->isCollection = null;
800
            $nuLink->isExpanded = null;
801
        } else {
802
            if (isset($nuLink->expandedResult->selfLink)) {
803
                $nuLink->expandedResult->selfLink->title = $propName;
804
                $nuLink->expandedResult->selfLink->url = $nuLink->url;
805
                $nuLink->expandedResult->title = new ODataTitle($propName);
806
                $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
807
            }
808
        }
809
    }
810
811
    /**
812
     * Gets collection of projection nodes under the current node.
813
     *
814
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
815
     *                                                        segment, If this method returns null it means no
816
     *                                                        projections are to be applied and the entire resource for
817
     *                                                        the current segment should be serialized, If it returns
818
     *                                                        non-null only the properties described by the returned
819
     *                                                        projection segments should be serialized
820
     */
821
    protected function getProjectionNodes()
822
    {
823
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
824
        if (null === $expandedProjectionNode || $expandedProjectionNode->canSelectAllProperties()) {
825
            return null;
826
        }
827
828
        return $expandedProjectionNode->getChildNodes();
829
    }
830
831
    /**
832
     * Find a 'ExpandedProjectionNode' instance in the projection tree
833
     * which describes the current segment.
834
     *
835
     * @return null|RootProjectionNode|ExpandedProjectionNode
836
     */
837 View Code Duplication
    protected function getCurrentExpandedProjectionNode()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
838
    {
839
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
840
        if (null === $expandedProjectionNode) {
841
            return null;
842
        } else {
843
            $segmentNames = $this->getStack()->getSegmentNames();
844
            $depth = count($segmentNames);
845
            // $depth == 1 means serialization of root entry
846
            //(the resource identified by resource path) is going on,
847
            //so control won't get into the below for loop.
848
            //we will directly return the root node,
849
            //which is 'ExpandedProjectionNode'
850
            // for resource identified by resource path.
851
            if (0 != $depth) {
852
                for ($i = 1; $i < $depth; ++$i) {
853
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]);
854
                    assert(null !== $expandedProjectionNode, 'is_null($expandedProjectionNode)');
855
                    assert(
856
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
857
                        '$expandedProjectionNode not instanceof ExpandedProjectionNode'
858
                    );
859
                }
860
            }
861
        }
862
863
        return $expandedProjectionNode;
864
    }
865
866
    /**
867
     * Builds the string corresponding to query parameters for top level results
868
     * (result set identified by the resource path) to be put in next page link.
869
     *
870
     * @return string|null string representing the query parameters in the URI
871
     *                     query parameter format, NULL if there
872
     *                     is no query parameters
873
     *                     required for the next link of top level result set
874
     */
875
    protected function getNextPageLinkQueryParametersForRootResourceSet()
876
    {
877
        $queryParameterString = null;
878
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
879
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
880
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
881
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
882
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
883
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
884
            if (null !== $value) {
885
                if (null !== $queryParameterString) {
886
                    $queryParameterString = $queryParameterString . '&';
887
                }
888
889
                $queryParameterString .= $queryOption . '=' . $value;
890
            }
891
        }
892
893
        $topCountValue = $this->getRequest()->getTopOptionCount();
894
        if (null !== $topCountValue) {
895
            $remainingCount = $topCountValue - $this->getRequest()->getTopCount();
896
            if (0 < $remainingCount) {
897
                if (null !== $queryParameterString) {
898
                    $queryParameterString .= '&';
899
                }
900
901
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
902
            }
903
        }
904
905
        if (null !== $queryParameterString) {
906
            $queryParameterString .= '&';
907
        }
908
909
        return $queryParameterString;
910
    }
911
912
    /**
913
     * @param $entryObject
914
     * @param $nonRelProp
915
     * @return ODataPropertyContent
916
     */
917
    private function writeProperties($entryObject, $nonRelProp)
918
    {
919
        $propertyContent = new ODataPropertyContent();
920
        foreach ($nonRelProp as $corn => $flake) {
921
            $resource = $nonRelProp[$corn]->getResourceType();
922
            if ($resource instanceof ResourceEntityType) {
923
                continue;
924
            }
925
            $result = $entryObject->$corn;
926
            $isBag = $flake->isKindOf(ResourcePropertyKind::BAG);
927
            $typePrepend = $isBag ? 'Collection(' : '';
928
            $typeAppend = $isBag ? ')' : '';
929
            $nonNull = null !== $result;
930
            $subProp = new ODataProperty();
931
            $subProp->name = strval($corn);
932
            $subProp->typeName = $typePrepend . $resource->getFullName() . $typeAppend;
933
934
            if ($nonNull && is_array($result)) {
935
                $subProp->value = $this->writeBagValue($resource, $result);
936 View Code Duplication
            } elseif ($resource instanceof ResourcePrimitiveType && $nonNull) {
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...
937
                $rType = $resource->getInstanceType();
938
                assert($rType instanceof IType, get_class($rType));
939
                $subProp->value = $this->primitiveToString($rType, $result);
940
            } elseif ($resource instanceof ResourceComplexType && $nonNull) {
941
                $subProp->value = $this->writeComplexValue($resource, $result, $flake->getName());
942
            }
943
            $propertyContent->properties[$corn] = $subProp;
944
        }
945
        return $propertyContent;
946
    }
947
948
    /**
949
     * @return void
950
     */
951
    private function loadStackIfEmpty()
952
    {
953
        if (0 == count($this->lightStack)) {
954
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
955
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
956
        }
957
    }
958
959
    /**
960
     * Convert the given primitive value to string.
961
     * Note: This method will not handle null primitive value.
962
     *
963
     * @param IType &$primitiveResourceType Type of the primitive property
964
     *                                      whose value need to be converted
965
     * @param mixed $primitiveValue         Primitive value to convert
966
     *
967
     * @return string
968
     */
969
    private function primitiveToString(IType &$type, $primitiveValue)
970
    {
971
        if ($type instanceof Boolean) {
972
            $stringValue = (true === $primitiveValue) ? 'true' : 'false';
973
        } elseif ($type instanceof Binary) {
974
            $stringValue = base64_encode($primitiveValue);
975
        } elseif ($type instanceof DateTime && $primitiveValue instanceof \DateTime) {
976
            $stringValue = $primitiveValue->format(\DateTime::ATOM);
977
        } elseif ($type instanceof StringType) {
978
            $stringValue = utf8_encode($primitiveValue);
979
        } else {
980
            $stringValue = strval($primitiveValue);
981
        }
982
983
        return $stringValue;
984
    }
985
986
    public static function isMatchPrimitive($resourceKind)
987
    {
988
        if (16 > $resourceKind) {
989
            return false;
990
        }
991
        if (28 < $resourceKind) {
992
            return false;
993
        }
994
        return 0 == ($resourceKind % 4);
995
    }
996
}
997