1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace POData\UriProcessor\QueryProcessor\SkipTokenParser; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
use POData\Common\Messages; |
7
|
|
|
use POData\Common\ODataException; |
8
|
|
|
use POData\Providers\Metadata\ResourceType; |
9
|
|
|
use POData\Providers\Metadata\Type\IType; |
10
|
|
|
use POData\Providers\Metadata\Type\Null1; |
11
|
|
|
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Class InternalSkipTokenInfo. |
15
|
|
|
* |
16
|
|
|
* Type which holds information about processed skiptoken value, this type |
17
|
|
|
* also provide method to search the given result set for the skiptoken |
18
|
|
|
* and to build skiptoken from an entry object. |
19
|
|
|
*/ |
20
|
|
|
class InternalSkipTokenInfo |
21
|
|
|
{ |
22
|
|
|
/** |
23
|
|
|
* Reference to an instance of InternalOrderByInfo which holds |
24
|
|
|
* sorter function(s) generated from orderby clause. |
25
|
|
|
* |
26
|
|
|
* @var InternalOrderByInfo |
27
|
|
|
*/ |
28
|
|
|
private $internalOrderByInfo; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Holds collection of values in the skiptoken corresponds to the orderby |
32
|
|
|
* path segments. |
33
|
|
|
* |
34
|
|
|
* @var array<array<IType>> |
35
|
|
|
*/ |
36
|
|
|
private $orderByValuesInSkipToken; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Holds reference to the type of the resource pointed by the request uri. |
40
|
|
|
* |
41
|
|
|
* @var ResourceType |
42
|
|
|
*/ |
43
|
|
|
private $resourceType; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Reference to the object holding parsed skiptoken value, this information |
47
|
|
|
* can be used by the IDSQP implementor for custom paging. |
48
|
|
|
* |
49
|
|
|
* @var SkipTokenInfo |
50
|
|
|
*/ |
51
|
|
|
private $skipTokenInfo; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Object which is used as a key for searching the sorted result, this object |
55
|
|
|
* will be an instance of type described by the resource type pointed by the |
56
|
|
|
* request uri. |
57
|
|
|
* |
58
|
|
|
* @var mixed |
59
|
|
|
*/ |
60
|
|
|
private $keyObject; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Creates a new instance of InternalSkipTokenInfo. |
64
|
|
|
* |
65
|
|
|
* @param InternalOrderByInfo &$internalOrderByInfo Reference to an instance of InternalOrderByInfo which holds |
66
|
|
|
* sorter function(s) generated from orderby clause |
67
|
|
|
* @param array<array<IType>> $orderByValuesInSkipToken Collection of values in the skiptoken corresponds to the |
68
|
|
|
* orderby path segments |
69
|
|
|
* @param ResourceType &$resourceType Reference to the resource type pointed to by the request uri |
70
|
|
|
*/ |
71
|
|
|
public function __construct( |
72
|
|
|
InternalOrderByInfo &$internalOrderByInfo, |
73
|
|
|
$orderByValuesInSkipToken, |
74
|
|
|
ResourceType &$resourceType |
75
|
|
|
) { |
76
|
|
|
$this->internalOrderByInfo = $internalOrderByInfo; |
77
|
|
|
$this->orderByValuesInSkipToken = $orderByValuesInSkipToken; |
78
|
|
|
$this->resourceType = $resourceType; |
79
|
|
|
$this->skipTokenInfo = null; |
80
|
|
|
$this->keyObject = null; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Gets reference to the SkipTokenInfo object holding result of |
85
|
|
|
* skiptoken parsing, which used by the IDSQP implementor for |
86
|
|
|
* custom paging. |
87
|
|
|
* |
88
|
|
|
* @return SkipTokenInfo |
89
|
|
|
*/ |
90
|
|
|
public function getSkipTokenInfo() |
91
|
|
|
{ |
92
|
|
|
if (null === $this->skipTokenInfo) { |
93
|
|
|
$orderbyInfo = $this->getInternalOrderByInfo()->getOrderByInfo(); |
94
|
|
|
$this->skipTokenInfo = new SkipTokenInfo( |
95
|
|
|
$orderbyInfo, |
96
|
|
|
$this->orderByValuesInSkipToken |
97
|
|
|
); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
return $this->skipTokenInfo; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Get reference to the InternalOrderByInfo object holding orderBy details. |
105
|
|
|
* |
106
|
|
|
* @return InternalOrderByInfo |
107
|
|
|
*/ |
108
|
|
|
public function getInternalOrderByInfo() |
109
|
|
|
{ |
110
|
|
|
return $this->internalOrderByInfo; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Search the sorted array of result set for key object created from the |
115
|
|
|
* skip token key values and returns index of first entry in the next page. |
116
|
|
|
* |
117
|
|
|
* @param array &$searchArray The sorted array to search |
118
|
|
|
* |
119
|
|
|
* @throws InvalidArgumentException |
120
|
|
|
* |
121
|
|
|
* @return int (1) If the array is empty then return -1, |
122
|
|
|
* (2) If the key object found then return index of first record |
123
|
|
|
* in the next page, |
124
|
|
|
* (3) If partial matching found (means found matching for first |
125
|
|
|
* m keys where m < n, where n is total number of positional |
126
|
|
|
* keys, then return the index of the object which has most matching |
127
|
|
|
*/ |
128
|
|
|
public function getIndexOfFirstEntryInTheNextPage(&$searchArray) |
129
|
|
|
{ |
130
|
|
|
if (!is_array($searchArray)) { |
131
|
|
|
$msg = Messages::internalSkipTokenInfoBinarySearchRequireArray('searchArray'); |
132
|
|
|
throw new \InvalidArgumentException($msg); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
if (empty($searchArray)) { |
136
|
|
|
return -1; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$comparer = $this->getInternalOrderByInfo()->getSorterFunction(); |
140
|
|
|
//Gets the key object initialized from skiptoken |
141
|
|
|
$keyObject = $this->getKeyObject(); |
142
|
|
|
$low = 0; |
143
|
|
|
$searchArraySize = count($searchArray) - 1; |
144
|
|
|
$high = $searchArraySize; |
145
|
|
|
do { |
146
|
|
|
$mid = $low + round(($high - $low)/2); |
147
|
|
|
$result = $comparer($keyObject, $searchArray[$mid]); |
148
|
|
|
if ($result > 0) { |
149
|
|
|
$low = $mid + 1; |
150
|
|
|
} elseif ($result < 0) { |
151
|
|
|
$high = $mid - 1; |
152
|
|
|
} else { |
153
|
|
|
//Now we found record the matches with skiptoken value, so first record of next page will at $mid + 1 |
154
|
|
|
if ($mid == $searchArraySize) { |
155
|
|
|
//Check skiptoken points to last record, in this case no more records available for next page |
156
|
|
|
return -1; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return $mid + 1; |
160
|
|
|
} |
161
|
|
|
} while ($low <= $high); |
162
|
|
|
|
163
|
|
|
if ($mid >= $searchArraySize) { |
164
|
|
|
//If key object does not match with last object, then no more page |
165
|
|
|
return -1; |
166
|
|
|
} elseif ($mid <= 0) { |
167
|
|
|
//If key object is less than first object, then paged result start from 0 |
168
|
|
|
return 0; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
//return index of the most matching object |
172
|
|
|
return $mid; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Gets the key object for searching, if the object is not initialized, |
177
|
|
|
* then do it from skiptoken positional values. |
178
|
|
|
* |
179
|
|
|
* @throws ODataException If reflection exception occurs while accessing or setting property |
180
|
|
|
* |
181
|
|
|
* @return mixed |
182
|
|
|
*/ |
183
|
|
|
public function getKeyObject() |
184
|
|
|
{ |
185
|
|
|
if (null === $this->keyObject) { |
186
|
|
|
$this->keyObject = $this->getInternalOrderByInfo()->getDummyObject(); |
187
|
|
|
$i = 0; |
188
|
|
|
foreach ($this->getInternalOrderByInfo()->getOrderByPathSegments() as $orderByPathSegment) { |
189
|
|
|
$index = 0; |
190
|
|
|
$currentObject = $this->keyObject; |
191
|
|
|
$subPathSegments = $orderByPathSegment->getSubPathSegments(); |
192
|
|
|
$subPathCount = count($subPathSegments); |
193
|
|
|
foreach ($subPathSegments as &$subPathSegment) { |
194
|
|
|
$isLastSegment = ($index == $subPathCount - 1); |
195
|
|
|
try { |
196
|
|
|
// if currentObject = null means, previous iteration did a |
197
|
|
|
// ReflectionProperty::getValue where ReflectionProperty |
198
|
|
|
// represents a complex/navigation, but its null, which means |
199
|
|
|
// the property is not set in the dummy object by OrderByParser, |
200
|
|
|
// an unexpected state. |
201
|
|
|
$subSegName = $subPathSegment->getName(); |
202
|
|
|
if (!$isLastSegment) { |
203
|
|
|
$currentObject = $this->resourceType->getPropertyValue($currentObject, $subSegName); |
204
|
|
|
} else { |
205
|
|
|
if ($this->orderByValuesInSkipToken[$i][1] instanceof Null1) { |
206
|
|
|
$this->resourceType->setPropertyValue($currentObject, $subPathSegment->getName(), null); |
207
|
|
|
} else { |
208
|
|
|
// The Lexer's Token::Text value will be always |
209
|
|
|
// string, convert the string to |
210
|
|
|
// required type i.e. int, float, double etc.. |
211
|
|
|
$value |
212
|
|
|
= $this->orderByValuesInSkipToken[$i][1]->convert( |
213
|
|
|
$this->orderByValuesInSkipToken[$i][0] |
214
|
|
|
); |
215
|
|
|
$this->resourceType->setPropertyValue($currentObject, $subSegName, $value); |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
} catch (\ReflectionException $reflectionException) { |
219
|
|
|
throw ODataException::createInternalServerError( |
220
|
|
|
Messages::internalSkipTokenInfoFailedToAccessOrInitializeProperty( |
221
|
|
|
$subPathSegment->getName() |
222
|
|
|
) |
223
|
|
|
); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
++$index; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
++$i; |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
return $this->keyObject; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* Build nextpage link from the given object which will be the last object |
238
|
|
|
* in the page. |
239
|
|
|
* |
240
|
|
|
* @param mixed $lastObject Entity instance to build next page link from |
241
|
|
|
* |
242
|
|
|
* @throws ODataException If reflection exception occurs while accessing |
243
|
|
|
* property |
244
|
|
|
* |
245
|
|
|
* @return string |
246
|
|
|
*/ |
247
|
|
|
public function buildNextPageLink($lastObject) |
248
|
|
|
{ |
249
|
|
|
$nextPageLink = null; |
250
|
|
|
foreach ($this->getInternalOrderByInfo()->getOrderByPathSegments() as $orderByPathSegment) { |
251
|
|
|
$index = 0; |
252
|
|
|
$currentObject = $lastObject; |
253
|
|
|
$subPathSegments = $orderByPathSegment->getSubPathSegments(); |
254
|
|
|
$subPathCount = count($subPathSegments); |
255
|
|
|
foreach ($subPathSegments as &$subPathSegment) { |
256
|
|
|
$isLastSegment = ($index == $subPathCount - 1); |
257
|
|
|
try { |
258
|
|
|
$currentObject = $this->resourceType->getPropertyValue($currentObject, $subPathSegment->getName()); |
259
|
|
|
if (null === $currentObject) { |
260
|
|
|
$nextPageLink .= 'null, '; |
261
|
|
|
break; |
262
|
|
View Code Duplication |
} elseif ($isLastSegment) { |
|
|
|
|
263
|
|
|
$type = $subPathSegment->getInstanceType(); |
264
|
|
|
assert($type instanceof IType, get_class($type)); |
265
|
|
|
$value = $type->convertToOData($currentObject); |
|
|
|
|
266
|
|
|
$nextPageLink .= $value . ', '; |
267
|
|
|
} |
268
|
|
|
} catch (\ReflectionException $reflectionException) { |
269
|
|
|
throw ODataException::createInternalServerError( |
270
|
|
|
Messages::internalSkipTokenInfoFailedToAccessOrInitializeProperty( |
271
|
|
|
$subPathSegment->getName() |
272
|
|
|
) |
273
|
|
|
); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
++$index; |
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
return rtrim($nextPageLink, ', '); |
281
|
|
|
} |
282
|
|
|
} |
283
|
|
|
|
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.