|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
|
|
4
|
|
|
namespace POData\ObjectModel; |
|
5
|
|
|
|
|
6
|
|
|
use POData\Common\ODataConstants; |
|
7
|
|
|
use POData\IService; |
|
8
|
|
|
use POData\Providers\ProvidersWrapper; |
|
9
|
|
|
use POData\Providers\Metadata\ResourceSetWrapper; |
|
10
|
|
|
use POData\Providers\Metadata\ResourceProperty; |
|
11
|
|
|
use POData\Providers\Metadata\ResourceTypeKind; |
|
12
|
|
|
use POData\Providers\Metadata\ResourceType; |
|
13
|
|
|
use POData\Providers\Metadata\Type\IType; |
|
14
|
|
|
use POData\UriProcessor\RequestDescription; |
|
15
|
|
|
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode; |
|
16
|
|
|
use POData\Common\InvalidOperationException; |
|
17
|
|
|
use POData\Common\ODataException; |
|
18
|
|
|
use POData\Common\Messages; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* Class ObjectModelSerializerBase |
|
22
|
|
|
* @package POData\ObjectModel |
|
23
|
|
|
*/ |
|
24
|
|
|
class ObjectModelSerializerBase |
|
25
|
|
|
{ |
|
26
|
|
|
/** |
|
27
|
|
|
* The service implementation. |
|
28
|
|
|
* |
|
29
|
|
|
* @var IService |
|
30
|
|
|
*/ |
|
31
|
|
|
protected $service; |
|
32
|
|
|
|
|
33
|
|
|
/** |
|
34
|
|
|
* Request description instance describes OData request the |
|
35
|
|
|
* the client has submitted and result of the request. |
|
36
|
|
|
* |
|
37
|
|
|
* @var RequestDescription |
|
38
|
|
|
*/ |
|
39
|
|
|
protected $request; |
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* Collection of segment names |
|
43
|
|
|
* Remark: Read 'ObjectModelSerializerNotes.txt' for more |
|
44
|
|
|
* details about segment. |
|
45
|
|
|
* |
|
46
|
|
|
* @var string[] |
|
47
|
|
|
*/ |
|
48
|
|
|
private $_segmentNames; |
|
49
|
|
|
|
|
50
|
|
|
/** |
|
51
|
|
|
* Collection of segment ResourceSetWrapper instances |
|
52
|
|
|
* Remark: Read 'ObjectModelSerializerNotes.txt' for more |
|
53
|
|
|
* details about segment. |
|
54
|
|
|
* |
|
55
|
|
|
* @var ResourceSetWrapper[] |
|
56
|
|
|
*/ |
|
57
|
|
|
private $_segmentResourceSetWrappers; |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* Result counts for segments |
|
61
|
|
|
* Remark: Read 'ObjectModelSerializerNotes.txt' for more |
|
62
|
|
|
* details about segment. |
|
63
|
|
|
* |
|
64
|
|
|
* @var int[] |
|
65
|
|
|
*/ |
|
66
|
|
|
private $_segmentResultCounts; |
|
67
|
|
|
|
|
68
|
|
|
/** |
|
69
|
|
|
* Collection of complex type instances used for cycle detection. |
|
70
|
|
|
* |
|
71
|
|
|
* @var array |
|
72
|
|
|
*/ |
|
73
|
|
|
protected $complexTypeInstanceCollection; |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* Absolute service Uri. |
|
77
|
|
|
* |
|
78
|
|
|
* @var string |
|
79
|
|
|
*/ |
|
80
|
|
|
protected $absoluteServiceUri; |
|
81
|
|
|
|
|
82
|
|
|
/** |
|
83
|
|
|
* Absolute service Uri with slash. |
|
84
|
|
|
* |
|
85
|
|
|
* @var string |
|
86
|
|
|
*/ |
|
87
|
|
|
protected $absoluteServiceUriWithSlash; |
|
88
|
|
|
|
|
89
|
|
|
/** |
|
90
|
|
|
* @param IService $service Reference to the data service instance. |
|
91
|
|
|
* @param RequestDescription $request Type instance describing the client submitted request. |
|
92
|
|
|
* |
|
93
|
|
|
*/ |
|
94
|
|
|
protected function __construct(IService $service, RequestDescription $request) |
|
95
|
|
|
{ |
|
96
|
|
|
$this->service = $service; |
|
97
|
|
|
$this->request = $request; |
|
98
|
|
|
$this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString(); |
|
99
|
|
|
$this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/'; |
|
100
|
|
|
$this->_segmentNames = array(); |
|
101
|
|
|
$this->_segmentResourceSetWrappers = array(); |
|
102
|
|
|
$this->_segmentResultCounts = array(); |
|
103
|
|
|
$this->complexTypeInstanceCollection = array(); |
|
104
|
|
|
} |
|
105
|
|
|
|
|
106
|
|
|
/** |
|
107
|
|
|
* Builds the key for the given entity instance. |
|
108
|
|
|
* Note: The generated key can be directly used in the uri, |
|
109
|
|
|
* this function will perform |
|
110
|
|
|
* required escaping of characters, for example: |
|
111
|
|
|
* Ships(ShipName='Antonio%20Moreno%20Taquer%C3%ADa',ShipID=123), |
|
112
|
|
|
* Note to method caller: Don't do urlencoding on |
|
113
|
|
|
* return value of this method as it already encoded. |
|
114
|
|
|
* |
|
115
|
|
|
* @param mixed $entityInstance Entity instance for which key value needs to be prepared. |
|
116
|
|
|
* @param ResourceType $resourceType Resource type instance containing metadata about the instance. |
|
117
|
|
|
* @param string $containerName Name of the entity set that the entity instance belongs to |
|
118
|
|
|
* . |
|
119
|
|
|
* |
|
120
|
|
|
* @return string Key for the given resource, with values encoded for use in a URI |
|
121
|
|
|
* . |
|
122
|
|
|
*/ |
|
123
|
|
|
protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName) |
|
124
|
|
|
{ |
|
125
|
|
|
$keyProperties = $resourceType->getKeyProperties(); |
|
126
|
|
|
$this->assert(count($keyProperties) != 0, 'count($keyProperties) != 0'); |
|
127
|
|
|
$keyString = $containerName . '('; |
|
128
|
|
|
$comma = null; |
|
129
|
|
|
foreach ($keyProperties as $keyName => $resourceProperty) { |
|
130
|
|
|
$keyType = $resourceProperty->getInstanceType(); |
|
131
|
|
|
if (!$keyType instanceof IType) continue; |
|
132
|
|
|
// $this->assert($keyType instanceof IType, '$keyType instanceof IType'); |
|
|
|
|
|
|
133
|
|
|
|
|
134
|
|
|
$keyValue = $this->getPropertyValue($entityInstance, $resourceType, $resourceProperty); |
|
135
|
|
|
if (is_null($keyValue)) { |
|
136
|
|
|
throw ODataException::createInternalServerError(Messages::badQueryNullKeysAreNotSupported($resourceType->getName(), $keyName)); |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
$keyValue = $keyType->convertToOData($keyValue); |
|
140
|
|
|
$keyString .= $comma . $keyName.'='.$keyValue; |
|
141
|
|
|
$comma = ','; |
|
142
|
|
|
} |
|
143
|
|
|
|
|
144
|
|
|
$keyString .= ')'; |
|
145
|
|
|
return $keyString; |
|
146
|
|
|
} |
|
147
|
|
|
|
|
148
|
|
|
/** |
|
149
|
|
|
* Get the value of a given property from an instance. |
|
150
|
|
|
* |
|
151
|
|
|
* @param mixed $entity Instance of a type which contains this property. |
|
152
|
|
|
* @param ResourceType $resourceType Resource type instance containing metadata about the instance. |
|
153
|
|
|
* @param ResourceProperty $resourceProperty Resource property instance containing metadata about the property whose value to be retrieved. |
|
154
|
|
|
* |
|
155
|
|
|
* @return mixed The value of the given property. |
|
156
|
|
|
* |
|
157
|
|
|
* @throws ODataException If reflection exception occurred while trying to access the property. |
|
158
|
|
|
* |
|
159
|
|
|
*/ |
|
160
|
|
|
protected function getPropertyValue($entity, ResourceType $resourceType, ResourceProperty $resourceProperty) |
|
161
|
|
|
{ |
|
162
|
|
|
try { |
|
163
|
|
|
//Is this slow? See #88 |
|
164
|
|
|
$reflectionClass = new \ReflectionClass(get_class($entity)); |
|
165
|
|
|
$reflectionProperty = $reflectionClass->getProperty($resourceProperty->getName()); |
|
166
|
|
|
$reflectionProperty->setAccessible(true); |
|
167
|
|
|
return $reflectionProperty->getValue($entity); |
|
168
|
|
|
} catch (\ReflectionException $reflectionException) { |
|
169
|
|
|
throw ODataException::createInternalServerError( |
|
170
|
|
|
Messages::objectModelSerializerFailedToAccessProperty( |
|
171
|
|
|
$resourceProperty->getName(), |
|
172
|
|
|
$resourceType->getName() |
|
173
|
|
|
) |
|
174
|
|
|
); |
|
175
|
|
|
} |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
/** |
|
179
|
|
|
* Resource set wrapper for the resource being serialized. |
|
180
|
|
|
* |
|
181
|
|
|
* @return ResourceSetWrapper |
|
182
|
|
|
*/ |
|
183
|
|
View Code Duplication |
protected function getCurrentResourceSetWrapper() |
|
|
|
|
|
|
184
|
|
|
{ |
|
185
|
|
|
$count = count($this->_segmentResourceSetWrappers); |
|
186
|
|
|
if ($count == 0) { |
|
187
|
|
|
return $this->request->getTargetResourceSetWrapper(); |
|
188
|
|
|
} else { |
|
189
|
|
|
return $this->_segmentResourceSetWrappers[$count - 1]; |
|
190
|
|
|
} |
|
191
|
|
|
} |
|
192
|
|
|
|
|
193
|
|
|
/** |
|
194
|
|
|
* Whether the current resource set is root resource set. |
|
195
|
|
|
* |
|
196
|
|
|
* @return boolean true if the current resource set root container else |
|
197
|
|
|
* false. |
|
198
|
|
|
*/ |
|
199
|
|
|
protected function isRootResourceSet() |
|
200
|
|
|
{ |
|
201
|
|
|
return empty($this->_segmentResourceSetWrappers) |
|
202
|
|
|
|| count($this->_segmentResourceSetWrappers) == 1; |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
/** |
|
206
|
|
|
* Returns the etag for the given resource. |
|
207
|
|
|
* |
|
208
|
|
|
* @param mixed $entryObject Resource for which etag value |
|
209
|
|
|
* needs to be returned |
|
210
|
|
|
* @param ResourceType $resourceType Resource type of the $entryObject |
|
211
|
|
|
* |
|
212
|
|
|
* @return string|null ETag value for the given resource |
|
213
|
|
|
* (with values encoded for use in a URI) |
|
214
|
|
|
* if there are etag properties, NULL if there is no etag property. |
|
215
|
|
|
*/ |
|
216
|
|
|
protected function getETagForEntry($entryObject, ResourceType $resourceType) |
|
217
|
|
|
{ |
|
218
|
|
|
$eTag = null; |
|
219
|
|
|
$comma = null; |
|
220
|
|
|
foreach ($resourceType->getETagProperties() as $eTagProperty) { |
|
221
|
|
|
$type = $eTagProperty->getInstanceType(); |
|
222
|
|
|
$this->assert( |
|
223
|
|
|
!is_null($type) && $type instanceof IType, |
|
224
|
|
|
'!is_null($type) && $type instanceof IType' |
|
225
|
|
|
); |
|
226
|
|
|
$value = $this->getPropertyValue($entryObject, $resourceType, $eTagProperty); |
|
227
|
|
View Code Duplication |
if (is_null($value)) { |
|
|
|
|
|
|
228
|
|
|
$eTag = $eTag . $comma. 'null'; |
|
229
|
|
|
} else { |
|
230
|
|
|
$eTag = $eTag . $comma . $type->convertToOData($value); |
|
|
|
|
|
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
$comma = ','; |
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
View Code Duplication |
if (!is_null($eTag)) { |
|
|
|
|
|
|
237
|
|
|
// If eTag is made up of datetime or string properties then the above |
|
238
|
|
|
// IType::converToOData will perform utf8 and url encode. But we don't |
|
239
|
|
|
// want this for eTag value. |
|
240
|
|
|
$eTag = urldecode(utf8_decode($eTag)); |
|
241
|
|
|
return ODataConstants::HTTP_WEAK_ETAG_PREFIX . rtrim($eTag, ',') . '"'; |
|
242
|
|
|
} |
|
243
|
|
|
|
|
244
|
|
|
return null; |
|
245
|
|
|
} |
|
246
|
|
|
|
|
247
|
|
|
/** |
|
248
|
|
|
* Pushes a segment for the root of the tree being written out |
|
249
|
|
|
* Note: Refer 'ObjectModelSerializerNotes.txt' for more details about |
|
250
|
|
|
* 'Segment Stack' and this method. |
|
251
|
|
|
* Note: Calls to this method should be balanced with calls to popSegment. |
|
252
|
|
|
* |
|
253
|
|
|
* @return bool true if the segment was pushed, false otherwise. |
|
254
|
|
|
*/ |
|
255
|
|
|
protected function pushSegmentForRoot() |
|
256
|
|
|
{ |
|
257
|
|
|
$segmentName = $this->request->getContainerName(); |
|
258
|
|
|
$segmentResourceSetWrapper = $this->request->getTargetResourceSetWrapper(); |
|
259
|
|
|
return $this->_pushSegment($segmentName, $segmentResourceSetWrapper); |
|
|
|
|
|
|
260
|
|
|
} |
|
261
|
|
|
|
|
262
|
|
|
/** |
|
263
|
|
|
* Pushes a segment for the current navigation property being written out. |
|
264
|
|
|
* Note: Refer 'ObjectModelSerializerNotes.txt' for more details about |
|
265
|
|
|
* 'Segment Stack' and this method. |
|
266
|
|
|
* Note: Calls to this method should be balanced with calls to popSegment. |
|
267
|
|
|
* |
|
268
|
|
|
* @param ResourceProperty &$resourceProperty The current navigation property |
|
269
|
|
|
* being written out. |
|
270
|
|
|
* |
|
271
|
|
|
* @return bool true if a segment was pushed, false otherwise |
|
272
|
|
|
* |
|
273
|
|
|
* @throws InvalidOperationException If this function invoked with non-navigation |
|
274
|
|
|
* property instance. |
|
275
|
|
|
*/ |
|
276
|
|
View Code Duplication |
protected function pushSegmentForNavigationProperty(ResourceProperty &$resourceProperty) |
|
|
|
|
|
|
277
|
|
|
{ |
|
278
|
|
|
if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY) { |
|
279
|
|
|
$this->assert(!empty($this->_segmentNames), '!is_empty($this->_segmentNames'); |
|
280
|
|
|
$currentResourceSetWrapper = $this->getCurrentResourceSetWrapper(); |
|
281
|
|
|
$currentResourceType = $currentResourceSetWrapper->getResourceType(); |
|
282
|
|
|
$currentResourceSetWrapper = $this->service |
|
283
|
|
|
->getProvidersWrapper() |
|
284
|
|
|
->getResourceSetWrapperForNavigationProperty( |
|
285
|
|
|
$currentResourceSetWrapper, |
|
|
|
|
|
|
286
|
|
|
$currentResourceType, |
|
287
|
|
|
$resourceProperty |
|
288
|
|
|
); |
|
289
|
|
|
|
|
290
|
|
|
$this->assert(!is_null($currentResourceSetWrapper), '!null($currentResourceSetWrapper)'); |
|
291
|
|
|
return $this->_pushSegment($resourceProperty->getName(), $currentResourceSetWrapper); |
|
|
|
|
|
|
292
|
|
|
} else { |
|
293
|
|
|
throw new InvalidOperationException('pushSegmentForNavigationProperty should not be called with non-entity type'); |
|
294
|
|
|
} |
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
/** |
|
298
|
|
|
* Gets collection of projection nodes under the current node. |
|
299
|
|
|
* |
|
300
|
|
|
* @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes |
|
301
|
|
|
* describing projections for the current segment, If this method returns |
|
302
|
|
|
* null it means no projections are to be applied and the entire resource |
|
303
|
|
|
* for the current segment should be serialized, If it returns non-null |
|
304
|
|
|
* only the properties described by the returned projection segments should |
|
305
|
|
|
* be serialized. |
|
306
|
|
|
*/ |
|
307
|
|
|
protected function getProjectionNodes() |
|
308
|
|
|
{ |
|
309
|
|
|
$expandedProjectionNode = $this->getCurrentExpandedProjectionNode(); |
|
310
|
|
|
if (is_null($expandedProjectionNode) |
|
311
|
|
|
|| $expandedProjectionNode->canSelectAllProperties() |
|
312
|
|
|
) { |
|
313
|
|
|
return null; |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
return $expandedProjectionNode->getChildNodes(); |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
/** |
|
320
|
|
|
* Find a 'ExpandedProjectionNode' instance in the projection tree |
|
321
|
|
|
* which describes the current segment. |
|
322
|
|
|
* |
|
323
|
|
|
* @return ExpandedProjectionNode|null |
|
324
|
|
|
*/ |
|
325
|
|
View Code Duplication |
protected function getCurrentExpandedProjectionNode() |
|
|
|
|
|
|
326
|
|
|
{ |
|
327
|
|
|
$expandedProjectionNode = $this->request->getRootProjectionNode(); |
|
328
|
|
|
if (is_null($expandedProjectionNode)) { |
|
329
|
|
|
return null; |
|
330
|
|
|
} else { |
|
331
|
|
|
$depth = count($this->_segmentNames); |
|
332
|
|
|
// $depth == 1 means serialization of root entry |
|
333
|
|
|
//(the resource identified by resource path) is going on, |
|
334
|
|
|
//so control won't get into the below for loop. |
|
335
|
|
|
//we will directly return the root node, |
|
336
|
|
|
//which is 'ExpandedProjectionNode' |
|
337
|
|
|
// for resource identified by resource path. |
|
338
|
|
|
if ($depth != 0) { |
|
339
|
|
|
for ($i = 1; $i < $depth; $i++) { |
|
340
|
|
|
$expandedProjectionNode |
|
341
|
|
|
= $expandedProjectionNode->findNode($this->_segmentNames[$i]); |
|
342
|
|
|
$this->assert( |
|
343
|
|
|
!is_null($expandedProjectionNode), |
|
344
|
|
|
'!is_null($expandedProjectionNode)' |
|
345
|
|
|
); |
|
346
|
|
|
$this->assert( |
|
347
|
|
|
$expandedProjectionNode instanceof ExpandedProjectionNode, |
|
348
|
|
|
'$expandedProjectionNode instanceof ExpandedProjectionNode' |
|
349
|
|
|
); |
|
350
|
|
|
} |
|
351
|
|
|
} |
|
352
|
|
|
} |
|
353
|
|
|
|
|
354
|
|
|
return $expandedProjectionNode; |
|
355
|
|
|
} |
|
356
|
|
|
|
|
357
|
|
|
/** |
|
358
|
|
|
* Check whether to expand a navigation property or not. |
|
359
|
|
|
* |
|
360
|
|
|
* @param string $navigationPropertyName Name of naviagtion property in question. |
|
361
|
|
|
* |
|
362
|
|
|
* @return boolean True if the given navigation should be |
|
363
|
|
|
* explanded otherwise false. |
|
364
|
|
|
*/ |
|
365
|
|
|
protected function shouldExpandSegment($navigationPropertyName) |
|
366
|
|
|
{ |
|
367
|
|
|
$expandedProjectionNode = $this->getCurrentExpandedProjectionNode(); |
|
368
|
|
|
if (is_null($expandedProjectionNode)) { |
|
369
|
|
|
return false; |
|
370
|
|
|
} |
|
371
|
|
|
|
|
372
|
|
|
$expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName); |
|
373
|
|
|
return !is_null($expandedProjectionNode) && ($expandedProjectionNode instanceof ExpandedProjectionNode); |
|
374
|
|
|
} |
|
375
|
|
|
|
|
376
|
|
|
/** |
|
377
|
|
|
* Pushes information about the segment that is going to be serialized |
|
378
|
|
|
* to the 'Segment Stack'. |
|
379
|
|
|
* Note: Refer 'ObjectModelSerializerNotes.txt' for more details about |
|
380
|
|
|
* 'Segment Stack' and this method. |
|
381
|
|
|
* Note: Calls to this method should be balanced with calls to popSegment. |
|
382
|
|
|
* |
|
383
|
|
|
* @param string $segmentName Name of segment to push. |
|
384
|
|
|
* @param ResourceSetWrapper &$resourceSetWrapper The resource set |
|
385
|
|
|
* wrapper to push. |
|
386
|
|
|
* |
|
387
|
|
|
* @return bool true if the segment was push, false otherwise |
|
388
|
|
|
*/ |
|
389
|
|
View Code Duplication |
private function _pushSegment($segmentName, ResourceSetWrapper &$resourceSetWrapper) |
|
|
|
|
|
|
390
|
|
|
{ |
|
391
|
|
|
$rootProjectionNode = $this->request->getRootProjectionNode(); |
|
392
|
|
|
// Even though there is no expand in the request URI, still we need to push |
|
393
|
|
|
// the segment information if we need to count |
|
394
|
|
|
//the number of entities written. |
|
395
|
|
|
// After serializing each entity we should check the count to see whether |
|
396
|
|
|
// we serialized more entities than configured |
|
397
|
|
|
//(page size, maxResultPerCollection). |
|
398
|
|
|
// But we will not do this check since library is doing paging and never |
|
399
|
|
|
// accumulate entities more than configured. |
|
400
|
|
|
// |
|
401
|
|
|
// if ((!is_null($rootProjectionNode) && $rootProjectionNode->isExpansionSpecified()) |
|
|
|
|
|
|
402
|
|
|
// || ($resourceSetWrapper->getResourceSetPageSize() != 0) |
|
|
|
|
|
|
403
|
|
|
// || ($this->service->getServiceConfiguration()->getMaxResultsPerCollection() != PHP_INT_MAX) |
|
|
|
|
|
|
404
|
|
|
//) {} |
|
405
|
|
|
|
|
406
|
|
|
if (!is_null($rootProjectionNode) |
|
407
|
|
|
&& $rootProjectionNode->isExpansionSpecified() |
|
408
|
|
|
) { |
|
409
|
|
|
array_push($this->_segmentNames, $segmentName); |
|
410
|
|
|
array_push($this->_segmentResourceSetWrappers, $resourceSetWrapper); |
|
411
|
|
|
array_push($this->_segmentResultCounts, 0); |
|
412
|
|
|
return true; |
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
return false; |
|
416
|
|
|
} |
|
417
|
|
|
|
|
418
|
|
|
/** |
|
419
|
|
|
* Get next page link from the given entity instance. |
|
420
|
|
|
* |
|
421
|
|
|
* @param mixed &$lastObject Last object serialized to be |
|
422
|
|
|
* used for generating $skiptoken. |
|
423
|
|
|
* @param string $absoluteUri Absolute response URI. |
|
424
|
|
|
* |
|
425
|
|
|
* @return URI for the link for next page. |
|
426
|
|
|
*/ |
|
427
|
|
|
protected function getNextLinkUri(&$lastObject, $absoluteUri) |
|
428
|
|
|
{ |
|
429
|
|
|
$currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode(); |
|
430
|
|
|
$internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo(); |
|
431
|
|
|
$skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject); |
|
432
|
|
|
$this->assert(!is_null($skipToken), '!is_null($skipToken)'); |
|
433
|
|
|
$queryParameterString = null; |
|
|
|
|
|
|
434
|
|
|
if ($this->isRootResourceSet()) { |
|
435
|
|
|
$queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet(); |
|
436
|
|
|
} else { |
|
437
|
|
|
$queryParameterString = $this->getNextPageLinkQueryParametersForExpandedResourceSet(); |
|
438
|
|
|
} |
|
439
|
|
|
|
|
440
|
|
|
$queryParameterString .= '$skiptoken=' . $skipToken; |
|
441
|
|
|
$odalaLink = new ODataLink(); |
|
442
|
|
|
$odalaLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING; |
|
443
|
|
|
$odalaLink->url = rtrim($absoluteUri, '/') . '?' . $queryParameterString; |
|
444
|
|
|
return $odalaLink; |
|
445
|
|
|
} |
|
446
|
|
|
|
|
447
|
|
|
/** |
|
448
|
|
|
* Builds the string corresponding to query parameters for top level results |
|
449
|
|
|
* (result set identified by the resource path) to be put in next page link. |
|
450
|
|
|
* |
|
451
|
|
|
* @return string|null string representing the query parameters in the URI |
|
452
|
|
|
* query parameter format, NULL if there |
|
453
|
|
|
* is no query parameters |
|
454
|
|
|
* required for the next link of top level result set. |
|
455
|
|
|
*/ |
|
456
|
|
|
protected function getNextPageLinkQueryParametersForRootResourceSet() |
|
457
|
|
|
{ |
|
458
|
|
|
$queryParameterString = null; |
|
459
|
|
|
foreach (array(ODataConstants::HTTPQUERY_STRING_FILTER, |
|
460
|
|
|
ODataConstants::HTTPQUERY_STRING_EXPAND, |
|
461
|
|
|
ODataConstants::HTTPQUERY_STRING_ORDERBY, |
|
462
|
|
|
ODataConstants::HTTPQUERY_STRING_INLINECOUNT, |
|
463
|
|
|
ODataConstants::HTTPQUERY_STRING_SELECT) as $queryOption |
|
464
|
|
|
) { |
|
465
|
|
|
$value = $this->service->getHost()->getQueryStringItem($queryOption); |
|
466
|
|
|
if (!is_null($value)) { |
|
467
|
|
|
if (!is_null($queryParameterString)) { |
|
468
|
|
|
$queryParameterString = $queryParameterString . '&'; |
|
469
|
|
|
} |
|
470
|
|
|
|
|
471
|
|
|
$queryParameterString .= $queryOption . '=' . $value; |
|
472
|
|
|
} |
|
473
|
|
|
} |
|
474
|
|
|
|
|
475
|
|
|
$topCountValue = $this->request->getTopOptionCount(); |
|
476
|
|
|
if (!is_null($topCountValue)) { |
|
477
|
|
|
$remainingCount = $topCountValue - $this->request->getTopCount(); |
|
478
|
|
|
if (!is_null($queryParameterString)) { |
|
479
|
|
|
$queryParameterString .= '&'; |
|
480
|
|
|
} |
|
481
|
|
|
|
|
482
|
|
|
$queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount; |
|
483
|
|
|
} |
|
484
|
|
|
|
|
485
|
|
|
if (!is_null($queryParameterString)) { |
|
486
|
|
|
$queryParameterString .= '&'; |
|
487
|
|
|
} |
|
488
|
|
|
|
|
489
|
|
|
return $queryParameterString; |
|
490
|
|
|
} |
|
491
|
|
|
|
|
492
|
|
|
/** |
|
493
|
|
|
* Builds the string corresponding to query parameters for current expanded |
|
494
|
|
|
* results to be put in next page link. |
|
495
|
|
|
* |
|
496
|
|
|
* @return string|null string representing the $select and $expand parameters |
|
497
|
|
|
* in the URI query parameter format, NULL if there is no |
|
498
|
|
|
* query parameters ($expand and/select) required for the |
|
499
|
|
|
* next link of expanded result set. |
|
500
|
|
|
*/ |
|
501
|
|
|
protected function getNextPageLinkQueryParametersForExpandedResourceSet() |
|
502
|
|
|
{ |
|
503
|
|
|
$queryParameterString = null; |
|
504
|
|
|
$expandedProjectionNode = $this->getCurrentExpandedProjectionNode(); |
|
505
|
|
|
if (!is_null($expandedProjectionNode)) { |
|
506
|
|
|
$pathSegments = array(); |
|
507
|
|
|
$selectionPaths = null; |
|
508
|
|
|
$expansionPaths = null; |
|
509
|
|
|
$foundSelections = false; |
|
510
|
|
|
$foundExpansions = false; |
|
511
|
|
|
$this->_buildSelectionAndExpansionPathsForNode( |
|
512
|
|
|
$pathSegments, |
|
513
|
|
|
$selectionPaths, |
|
514
|
|
|
$expansionPaths, |
|
515
|
|
|
$expandedProjectionNode, |
|
516
|
|
|
$foundSelections, |
|
517
|
|
|
$foundExpansions |
|
518
|
|
|
); |
|
519
|
|
|
|
|
520
|
|
|
if ($foundSelections && $expandedProjectionNode->canSelectAllProperties()) { |
|
521
|
|
|
$this->_appendSelectionOrExpandPath($selectionPaths, $pathSegments, '*'); |
|
522
|
|
|
} |
|
523
|
|
|
|
|
524
|
|
|
if (!is_null($selectionPaths)) { |
|
525
|
|
|
$queryParameterString = '$select=' . $selectionPaths; |
|
526
|
|
|
} |
|
527
|
|
|
|
|
528
|
|
|
if (!is_null($expansionPaths)) { |
|
529
|
|
|
if (!is_null($queryParameterString)) { |
|
530
|
|
|
$queryParameterString .= '&'; |
|
531
|
|
|
} |
|
532
|
|
|
|
|
533
|
|
|
$queryParameterString = '$expand=' . $expansionPaths; |
|
534
|
|
|
} |
|
535
|
|
|
|
|
536
|
|
|
if (!is_null($queryParameterString)) { |
|
537
|
|
|
$queryParameterString .= '&'; |
|
538
|
|
|
} |
|
539
|
|
|
} |
|
540
|
|
|
|
|
541
|
|
|
return $queryParameterString; |
|
542
|
|
|
} |
|
543
|
|
|
|
|
544
|
|
|
/** |
|
545
|
|
|
* Wheter next link is needed for the current resource set (feed) |
|
546
|
|
|
* being serialized. |
|
547
|
|
|
* |
|
548
|
|
|
* @param int $resultSetCount Number of entries in the current |
|
549
|
|
|
* resource set. |
|
550
|
|
|
* |
|
551
|
|
|
* @return boolean true if the feed must have a next page link |
|
552
|
|
|
*/ |
|
553
|
|
|
protected function needNextPageLink($resultSetCount) |
|
554
|
|
|
{ |
|
555
|
|
|
$currentResourceSet = $this->getCurrentResourceSetWrapper(); |
|
556
|
|
|
$recursionLevel = count($this->_segmentNames); |
|
557
|
|
|
//$this->assert($recursionLevel != 0, '$recursionLevel != 0'); |
|
|
|
|
|
|
558
|
|
|
$pageSize = $currentResourceSet->getResourceSetPageSize(); |
|
559
|
|
|
|
|
560
|
|
|
if ($recursionLevel == 1) { |
|
561
|
|
|
//presence of $top option affect next link for root container |
|
562
|
|
|
$topValueCount = $this->request->getTopOptionCount(); |
|
563
|
|
|
if (!is_null($topValueCount) && ($topValueCount <= $pageSize)) { |
|
564
|
|
|
return false; |
|
565
|
|
|
} |
|
566
|
|
|
} |
|
567
|
|
|
|
|
568
|
|
|
return $resultSetCount == $pageSize; |
|
569
|
|
|
} |
|
570
|
|
|
|
|
571
|
|
|
/** |
|
572
|
|
|
* Pops segment information from the 'Segment Stack' |
|
573
|
|
|
* Note: Refer 'ObjectModelSerializerNotes.txt' for more details about |
|
574
|
|
|
* 'Segment Stack' and this method. |
|
575
|
|
|
* Note: Calls to this method should be balanced with previous |
|
576
|
|
|
* calls to _pushSegment. |
|
577
|
|
|
* |
|
578
|
|
|
* @param boolean $needPop Is a pop required. Only true if last |
|
579
|
|
|
* push was successful. |
|
580
|
|
|
* |
|
581
|
|
|
* @return void |
|
582
|
|
|
* |
|
583
|
|
|
* @throws InvalidOperationException If found un-balanced call with _pushSegment |
|
584
|
|
|
*/ |
|
585
|
|
View Code Duplication |
protected function popSegment($needPop) |
|
|
|
|
|
|
586
|
|
|
{ |
|
587
|
|
|
if ($needPop) { |
|
588
|
|
|
if (!empty($this->_segmentNames)) { |
|
589
|
|
|
array_pop($this->_segmentNames); |
|
590
|
|
|
array_pop($this->_segmentResourceSetWrappers); |
|
591
|
|
|
array_pop($this->_segmentResultCounts); |
|
592
|
|
|
} else { |
|
593
|
|
|
throw new InvalidOperationException('Found non-balanced call to _pushSegment and popSegment'); |
|
594
|
|
|
} |
|
595
|
|
|
} |
|
596
|
|
|
} |
|
597
|
|
|
|
|
598
|
|
|
/** |
|
599
|
|
|
* Recursive metod to build $expand and $select paths for a specified node. |
|
600
|
|
|
* |
|
601
|
|
|
* @param string[] &$parentPathSegments Array of path |
|
602
|
|
|
* segments which leads |
|
603
|
|
|
* up to (including) |
|
604
|
|
|
* the segment |
|
605
|
|
|
* represented by |
|
606
|
|
|
* $expandedProjectionNode. |
|
607
|
|
|
* @param string[] &$selectionPaths The string which |
|
608
|
|
|
* holds projection |
|
609
|
|
|
* path segment |
|
610
|
|
|
* seperated by comma, |
|
611
|
|
|
* On return this argument |
|
612
|
|
|
* will be updated with |
|
613
|
|
|
* the selection path |
|
614
|
|
|
* segments under |
|
615
|
|
|
* this node. |
|
616
|
|
|
* @param string[] &$expansionPaths The string which holds |
|
617
|
|
|
* expansion path segment |
|
618
|
|
|
* seperated by comma. |
|
619
|
|
|
* On return this argument |
|
620
|
|
|
* will be updated with |
|
621
|
|
|
* the expand path |
|
622
|
|
|
* segments under |
|
623
|
|
|
* this node. |
|
624
|
|
|
* @param ExpandedProjectionNode &$expandedProjectionNode The expanded node for |
|
625
|
|
|
* which expansion |
|
626
|
|
|
* and selection path |
|
627
|
|
|
* to be build. |
|
628
|
|
|
* @param boolean &$foundSelections On return, this |
|
629
|
|
|
* argument will hold |
|
630
|
|
|
* true if any selection |
|
631
|
|
|
* defined under this node |
|
632
|
|
|
* false otherwise. |
|
633
|
|
|
* @param boolean &$foundExpansions On return, this |
|
634
|
|
|
* argument will hold |
|
635
|
|
|
* true if any expansion |
|
636
|
|
|
* defined under this node |
|
637
|
|
|
* false otherwise. |
|
638
|
|
|
* |
|
639
|
|
|
* @return void |
|
640
|
|
|
*/ |
|
641
|
|
|
private function _buildSelectionAndExpansionPathsForNode(&$parentPathSegments, |
|
642
|
|
|
&$selectionPaths, &$expansionPaths, |
|
643
|
|
|
ExpandedProjectionNode &$expandedProjectionNode, |
|
644
|
|
|
&$foundSelections, &$foundExpansions |
|
645
|
|
|
) { |
|
646
|
|
|
$foundSelections = false; |
|
647
|
|
|
$foundExpansions = false; |
|
648
|
|
|
$foundSelectionOnChild = false; |
|
649
|
|
|
$foundExpansionOnChild = false; |
|
650
|
|
|
$expandedChildrenNeededToBeSelected = array(); |
|
651
|
|
|
foreach ($expandedProjectionNode->getChildNodes() as $childNode) { |
|
652
|
|
|
if (!($childNode instanceof ExpandedProjectionNode)) { |
|
653
|
|
|
$foundSelections = true; |
|
654
|
|
|
$this->_appendSelectionOrExpandPath( |
|
655
|
|
|
$selectionPaths, |
|
656
|
|
|
$parentPathSegments, |
|
657
|
|
|
$childNode->getPropertyName() |
|
658
|
|
|
); |
|
659
|
|
|
} else { |
|
660
|
|
|
$foundExpansions = true; |
|
661
|
|
|
array_push($parentPathSegments, $childNode->getPropertyName()); |
|
662
|
|
|
$this->_buildSelectionAndExpansionPathsForNode( |
|
663
|
|
|
$parentPathSegments, |
|
664
|
|
|
$selectionPaths, $expansionPaths, |
|
665
|
|
|
$childNode, $foundSelectionOnChild, |
|
666
|
|
|
$foundExpansionOnChild |
|
667
|
|
|
); |
|
668
|
|
|
array_pop($parentPathSegments); |
|
669
|
|
|
if ($childNode->canSelectAllProperties()) { |
|
670
|
|
|
if ($foundSelectionOnChild) { |
|
671
|
|
|
$this->_appendSelectionOrExpandPath( |
|
672
|
|
|
$selectionPaths, |
|
673
|
|
|
$parentPathSegments, |
|
674
|
|
|
$childNode->getPropertyName() . '/*' |
|
675
|
|
|
); |
|
676
|
|
|
} else { |
|
677
|
|
|
$expandedChildrenNeededToBeSelected[] = $childNode; |
|
678
|
|
|
} |
|
679
|
|
|
} |
|
680
|
|
|
} |
|
681
|
|
|
|
|
682
|
|
|
$foundSelections |= $foundSelectionOnChild; |
|
683
|
|
|
if (!$foundExpansionOnChild) { |
|
684
|
|
|
$this->_appendSelectionOrExpandPath( |
|
685
|
|
|
$expansionPaths, |
|
686
|
|
|
$parentPathSegments, |
|
687
|
|
|
$childNode->getPropertyName() |
|
688
|
|
|
); |
|
689
|
|
|
} |
|
690
|
|
|
} |
|
691
|
|
|
|
|
692
|
|
|
if (!$expandedProjectionNode->canSelectAllProperties() || $foundSelections) { |
|
693
|
|
|
foreach ($expandedChildrenNeededToBeSelected as $childToProject) { |
|
694
|
|
|
$this->_appendSelectionOrExpandPath( |
|
695
|
|
|
$selectionPaths, |
|
696
|
|
|
$parentPathSegments, |
|
697
|
|
|
$childNode->getPropertyName() |
|
|
|
|
|
|
698
|
|
|
); |
|
699
|
|
|
$foundSelections = true; |
|
700
|
|
|
} |
|
701
|
|
|
} |
|
702
|
|
|
} |
|
703
|
|
|
|
|
704
|
|
|
/** |
|
705
|
|
|
* Append the given path to $expand or $select path list. |
|
706
|
|
|
* |
|
707
|
|
|
* @param string &$path The $expand or $select path list to which to append the given path. |
|
708
|
|
|
* @param string[] &$parentPathSegments The list of path up to the $segmentToAppend. |
|
709
|
|
|
* @param string $segmentToAppend The last segment of the path. |
|
710
|
|
|
* |
|
711
|
|
|
* @return void |
|
712
|
|
|
*/ |
|
713
|
|
|
private function _appendSelectionOrExpandPath(&$path, &$parentPathSegments, $segmentToAppend) |
|
714
|
|
|
{ |
|
715
|
|
|
if (!is_null($path)) { |
|
716
|
|
|
$path .= ', '; |
|
717
|
|
|
} |
|
718
|
|
|
|
|
719
|
|
|
foreach ($parentPathSegments as $parentPathSegment) { |
|
720
|
|
|
$path .= $parentPathSegment . '/'; |
|
721
|
|
|
} |
|
722
|
|
|
|
|
723
|
|
|
$path .= $segmentToAppend; |
|
724
|
|
|
} |
|
725
|
|
|
|
|
726
|
|
|
/** |
|
727
|
|
|
* Assert that the given condition is true. |
|
728
|
|
|
* |
|
729
|
|
|
* @param boolean $condition Condition to be asserted. |
|
730
|
|
|
* @param string $conditionAsString String containing message incase |
|
731
|
|
|
* if assertion fails. |
|
732
|
|
|
* |
|
733
|
|
|
* @throws InvalidOperationException Incase if assertion failes. |
|
734
|
|
|
* |
|
735
|
|
|
* @return void |
|
736
|
|
|
*/ |
|
737
|
|
|
protected function assert($condition, $conditionAsString) |
|
738
|
|
|
{ |
|
739
|
|
|
if (!$condition) { |
|
740
|
|
|
throw new InvalidOperationException("Unexpected state, expecting $conditionAsString"); |
|
741
|
|
|
} |
|
742
|
|
|
} |
|
743
|
|
|
} |
|
744
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.