1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace POData; |
4
|
|
|
|
5
|
|
|
use POData\Common\MimeTypes; |
6
|
|
|
use POData\Common\ReflectionHandler; |
7
|
|
|
use POData\Common\Version; |
8
|
|
|
use POData\Configuration\IServiceConfiguration; |
9
|
|
|
use POData\ObjectModel\IObjectSerialiser; |
10
|
|
|
use POData\ObjectModel\ODataFeed; |
11
|
|
|
use POData\ObjectModel\ODataURLCollection; |
12
|
|
|
use POData\OperationContext\HTTPRequestMethod; |
13
|
|
|
use POData\Common\ErrorHandler; |
14
|
|
|
use POData\Common\Messages; |
15
|
|
|
use POData\Common\ODataException; |
16
|
|
|
use POData\Common\ODataConstants; |
17
|
|
|
use POData\Common\NotImplementedException; |
18
|
|
|
use POData\Common\InvalidOperationException; |
19
|
|
|
use POData\Common\HttpStatus; |
20
|
|
|
use POData\Providers\Metadata\Type\IType; |
21
|
|
|
use POData\Providers\ProvidersWrapper; |
22
|
|
|
use POData\Providers\Stream\StreamProviderWrapper; |
23
|
|
|
use POData\Configuration\ServiceConfiguration; |
24
|
|
|
use POData\UriProcessor\UriProcessor; |
25
|
|
|
use POData\UriProcessor\RequestDescription; |
26
|
|
|
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind; |
27
|
|
|
use POData\OperationContext\ServiceHost; |
28
|
|
|
use POData\Providers\Metadata\ResourceType; |
29
|
|
|
use POData\Providers\Metadata\Type\Binary; |
30
|
|
|
use POData\ObjectModel\ObjectModelSerializer; |
31
|
|
|
use POData\Writers\Atom\AtomODataWriter; |
32
|
|
|
use POData\Writers\Json\JsonLightMetadataLevel; |
33
|
|
|
use POData\Writers\Json\JsonLightODataWriter; |
34
|
|
|
use POData\Writers\Json\JsonODataV1Writer; |
35
|
|
|
use POData\Writers\Json\JsonODataV2Writer; |
36
|
|
|
use POData\Writers\ResponseWriter; |
37
|
|
|
use POData\Providers\Query\IQueryProvider; |
38
|
|
|
use POData\Providers\Metadata\IMetadataProvider; |
39
|
|
|
use POData\OperationContext\IOperationContext; |
40
|
|
|
use POData\Writers\ODataWriterRegistry; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Class BaseService. |
44
|
|
|
* |
45
|
|
|
* The base class for all BaseService specific classes. This class implements |
46
|
|
|
* the following interfaces: |
47
|
|
|
* (1) IRequestHandler |
48
|
|
|
* Implementing this interface requires defining the function |
49
|
|
|
* 'handleRequest' that will be invoked by dispatcher |
50
|
|
|
* (2) IService |
51
|
|
|
* Force BaseService class to implement functions for custom |
52
|
|
|
* data service providers |
53
|
|
|
*/ |
54
|
|
|
|
55
|
|
|
abstract class BaseService implements IRequestHandler, IService |
56
|
|
|
{ |
57
|
|
|
/** |
58
|
|
|
* The wrapper over IQueryProvider and IMetadataProvider implementations. |
59
|
|
|
* |
60
|
|
|
* @var ProvidersWrapper |
61
|
|
|
*/ |
62
|
|
|
private $providersWrapper; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* The wrapper over IStreamProvider implementation. |
66
|
|
|
* |
67
|
|
|
* @var StreamProviderWrapper |
68
|
|
|
*/ |
69
|
|
|
protected $streamProvider; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Hold reference to the ServiceHost instance created by dispatcher, |
73
|
|
|
* using this library can access headers and body of Http Request |
74
|
|
|
* dispatcher received and the Http Response Dispatcher is going to send. |
75
|
|
|
* |
76
|
|
|
* @var ServiceHost |
77
|
|
|
*/ |
78
|
|
|
private $_serviceHost; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* To hold reference to ServiceConfiguration instance where the |
82
|
|
|
* service specific rules (page limit, resource set access rights |
83
|
|
|
* etc...) are defined. |
84
|
|
|
* |
85
|
|
|
* @var IServiceConfiguration |
86
|
|
|
*/ |
87
|
|
|
protected $config; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Hold reference to object serialiser - bit wot turns PHP objects |
91
|
|
|
* into message traffic on wire |
92
|
|
|
* |
93
|
|
|
* @var IObjectSerialiser |
94
|
|
|
*/ |
95
|
|
|
protected $objectSerialiser; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Get reference to object serialiser - bit wot turns PHP objects |
99
|
|
|
* into message traffic on wire |
100
|
|
|
* |
101
|
|
|
* @var IObjectSerialiser |
102
|
|
|
*/ |
103
|
|
|
public function getObjectSerialiser() |
104
|
|
|
{ |
105
|
|
|
assert(null != $this->objectSerialiser); |
106
|
|
|
return $this->objectSerialiser; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
protected function __construct(IObjectSerialiser $serialiser = null) |
110
|
|
|
{ |
111
|
|
|
$this->objectSerialiser = (null == $serialiser) ? new ObjectModelSerializer($this, null) : $serialiser; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Gets reference to ServiceConfiguration instance so that |
116
|
|
|
* service specific rules defined by the developer can be |
117
|
|
|
* accessed. |
118
|
|
|
* |
119
|
|
|
* @return IServiceConfiguration |
120
|
|
|
*/ |
121
|
|
|
public function getConfiguration() |
122
|
|
|
{ |
123
|
|
|
assert(null != $this->config); |
124
|
|
|
return $this->config; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
//TODO: shouldn't we hide this from the interface..if we need it at all. |
128
|
|
|
/** |
129
|
|
|
* Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation. |
130
|
|
|
* |
131
|
|
|
* @return ProvidersWrapper |
132
|
|
|
*/ |
133
|
|
|
public function getProvidersWrapper() |
134
|
|
|
{ |
135
|
|
|
return $this->providersWrapper; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Gets reference to wrapper class instance over IDSSP implementation. |
140
|
|
|
* |
141
|
|
|
* @return StreamProviderWrapper |
142
|
|
|
*/ |
143
|
|
|
public function getStreamProviderWrapper() |
144
|
|
|
{ |
145
|
|
|
return $this->streamProvider; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Get reference to the data service host instance. |
150
|
|
|
* |
151
|
|
|
* @return ServiceHost |
152
|
|
|
*/ |
153
|
|
|
public function getHost() |
154
|
|
|
{ |
155
|
|
|
assert(null != $this->_serviceHost); |
156
|
|
|
return $this->_serviceHost; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Sets the data service host instance. |
161
|
|
|
* |
162
|
|
|
* @param ServiceHost $serviceHost The data service host instance |
163
|
|
|
*/ |
164
|
|
|
public function setHost(ServiceHost $serviceHost) |
165
|
|
|
{ |
166
|
|
|
$this->_serviceHost = $serviceHost; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* To get reference to operation context where we have direct access to |
171
|
|
|
* headers and body of Http Request, we have received and the Http Response |
172
|
|
|
* We are going to send. |
173
|
|
|
* |
174
|
|
|
* @return IOperationContext |
175
|
|
|
*/ |
176
|
|
|
public function getOperationContext() |
177
|
|
|
{ |
178
|
|
|
return $this->getHost()->getOperationContext(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Get reference to the wrapper over IStreamProvider or |
183
|
|
|
* IStreamProvider2 implementations. |
184
|
|
|
* |
185
|
|
|
* @return StreamProviderWrapper |
186
|
|
|
*/ |
187
|
|
|
public function getStreamProvider() |
188
|
|
|
{ |
189
|
|
|
if (is_null($this->streamProvider)) { |
190
|
|
|
$this->streamProvider = new StreamProviderWrapper(); |
191
|
|
|
$this->streamProvider->setService($this); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
return $this->streamProvider; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Top-level handler invoked by Dispatcher against any request to this |
199
|
|
|
* service. This method will hand over request processing task to other |
200
|
|
|
* functions which process the request, set required headers and Response |
201
|
|
|
* stream (if any in Atom/Json format) in |
202
|
|
|
* WebOperationContext::Current()::OutgoingWebResponseContext. |
203
|
|
|
* Once this function returns, dispatcher uses global WebOperationContext |
204
|
|
|
* to write out the request response to client. |
205
|
|
|
* This function will perform the following operations: |
206
|
|
|
* (1) Check whether the top level service class implements |
207
|
|
|
* IServiceProvider which means the service is a custom service, in |
208
|
|
|
* this case make sure the top level service class implements |
209
|
|
|
* IMetaDataProvider and IQueryProvider. |
210
|
|
|
* These are the minimal interfaces that a custom service to be |
211
|
|
|
* implemented in order to expose its data as OData. Save reference to |
212
|
|
|
* These interface implementations. |
213
|
|
|
* NOTE: Here we will ensure only providers for IDSQP and IDSMP. The |
214
|
|
|
* IDSSP will be ensured only when there is an GET request on MLE/Named |
215
|
|
|
* stream. |
216
|
|
|
* |
217
|
|
|
* (2). Invoke 'Initialize' method of top level service for |
218
|
|
|
* collecting the configuration rules set by the developer for this |
219
|
|
|
* service. |
220
|
|
|
* |
221
|
|
|
* (3). Invoke the Uri processor to process the request URI. The uri |
222
|
|
|
* processor will do the following: |
223
|
|
|
* (a). Validate the request uri syntax using OData uri rules |
224
|
|
|
* (b). Validate the request using metadata of this service |
225
|
|
|
* (c). Parse the request uri and using, IQueryProvider |
226
|
|
|
* implementation, fetches the resources pointed by the uri |
227
|
|
|
* if required |
228
|
|
|
* (d). Build a RequestDescription which encapsulate everything |
229
|
|
|
* related to request uri (e.g. type of resource, result |
230
|
|
|
* etc...) |
231
|
|
|
* (3). Invoke handleRequest2 for further processing |
232
|
|
|
*/ |
233
|
|
|
public function handleRequest() |
234
|
|
|
{ |
235
|
|
|
try { |
236
|
|
|
$this->createProviders(); |
237
|
|
|
$this->getHost()->validateQueryParameters(); |
238
|
|
|
//$requestMethod = $this->getOperationContext()->incomingRequest()->getMethod(); |
|
|
|
|
239
|
|
|
//if ($requestMethod != HTTPRequestMethod::GET()) { |
|
|
|
|
240
|
|
|
// Now supporting GET and trying to support PUT |
241
|
|
|
//throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod)); |
|
|
|
|
242
|
|
|
//} |
243
|
|
|
|
244
|
|
|
$uriProcessor = UriProcessor::process($this); |
245
|
|
|
$request = $uriProcessor->getRequest(); |
246
|
|
|
$this->serializeResult($request, $uriProcessor); |
247
|
|
|
} catch (\Exception $exception) { |
248
|
|
|
ErrorHandler::handleException($exception, $this); |
249
|
|
|
// Return to dispatcher for writing serialized exception |
250
|
|
|
return; |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* @return IQueryProvider |
256
|
|
|
*/ |
257
|
|
|
abstract public function getQueryProvider(); |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @return IMetadataProvider |
261
|
|
|
*/ |
262
|
|
|
abstract public function getMetadataProvider(); |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @return \POData\Providers\Stream\IStreamProvider |
266
|
|
|
*/ |
267
|
|
|
abstract public function getStreamProviderX(); |
268
|
|
|
|
269
|
|
|
/** @var ODataWriterRegistry */ |
270
|
|
|
protected $writerRegistry; |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Returns the ODataWriterRegistry to use when writing the response to a service document or resource request. |
274
|
|
|
* |
275
|
|
|
* @return ODataWriterRegistry |
276
|
|
|
*/ |
277
|
|
|
public function getODataWriterRegistry() |
278
|
|
|
{ |
279
|
|
|
assert(null != $this->writerRegistry); |
280
|
|
|
return $this->writerRegistry; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes |
285
|
|
|
* BaseService::Initialize to initialize service specific policies. |
286
|
|
|
* |
287
|
|
|
* @throws ODataException |
288
|
|
|
*/ |
289
|
|
|
protected function createProviders() |
290
|
|
|
{ |
291
|
|
|
$metadataProvider = $this->getMetadataProvider(); |
292
|
|
|
if (is_null($metadataProvider)) { |
293
|
|
|
throw ODataException::createInternalServerError(Messages::providersWrapperNull()); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
if (!$metadataProvider instanceof IMetadataProvider) { |
297
|
|
|
throw ODataException::createInternalServerError(Messages::invalidMetadataInstance()); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
$queryProvider = $this->getQueryProvider(); |
301
|
|
|
|
302
|
|
|
if (is_null($queryProvider)) { |
303
|
|
|
throw ODataException::createInternalServerError(Messages::providersWrapperNull()); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
if (!$queryProvider instanceof IQueryProvider) { |
307
|
|
|
throw ODataException::createInternalServerError(Messages::invalidQueryInstance()); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
$this->config = new ServiceConfiguration($metadataProvider); |
311
|
|
|
$this->providersWrapper = new ProvidersWrapper( |
312
|
|
|
$metadataProvider, |
313
|
|
|
$queryProvider, |
314
|
|
|
$this->config |
315
|
|
|
); |
316
|
|
|
|
317
|
|
|
$this->initialize($this->config); |
318
|
|
|
|
319
|
|
|
//TODO: this seems like a bad spot to do this |
320
|
|
|
$this->writerRegistry = new ODataWriterRegistry(); |
321
|
|
|
$this->registerWriters(); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
//TODO: i don't want this to be public..but it's the only way to test it right now... |
325
|
|
|
public function registerWriters() |
326
|
|
|
{ |
327
|
|
|
$registry = $this->getODataWriterRegistry(); |
328
|
|
|
$serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion(); |
329
|
|
|
$serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString(); |
330
|
|
|
|
331
|
|
|
//We always register the v1 stuff |
332
|
|
|
$registry->register(new JsonODataV1Writer()); |
333
|
|
|
$registry->register(new AtomODataWriter($serviceURI)); |
334
|
|
|
|
335
|
|
|
if (-1 < $serviceVersion->compare(Version::v2())) { |
336
|
|
|
$registry->register(new JsonODataV2Writer()); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
if (-1 < $serviceVersion->compare(Version::v3())) { |
340
|
|
|
$registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::NONE(), $serviceURI)); |
341
|
|
|
$registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::MINIMAL(), $serviceURI)); |
342
|
|
|
$registry->register(new JsonLightODataWriter(JsonLightMetadataLevel::FULL(), $serviceURI)); |
343
|
|
|
} |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Serialize the requested resource. |
348
|
|
|
* |
349
|
|
|
* @param RequestDescription $request The description of the request submitted by the client |
350
|
|
|
* @param UriProcessor $uriProcessor Reference to the uri processor |
351
|
|
|
*/ |
352
|
|
|
protected function serializeResult(RequestDescription $request, UriProcessor $uriProcessor) |
353
|
|
|
{ |
354
|
|
|
$isETagHeaderAllowed = $request->isETagHeaderAllowed(); |
355
|
|
|
if ($this->getConfiguration()->getValidateETagHeader() && !$isETagHeaderAllowed) { |
356
|
|
|
if (!is_null($this->getHost()->getRequestIfMatch()) |
357
|
|
|
|| !is_null($this->getHost()->getRequestIfNoneMatch()) |
358
|
|
|
) { |
359
|
|
|
throw ODataException::createBadRequestError( |
360
|
|
|
Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString()) |
361
|
|
|
); |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
$responseContentType = $this->getResponseContentType($request, $uriProcessor); |
366
|
|
|
|
367
|
|
|
if (is_null($responseContentType) && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) { |
368
|
|
|
//the responseContentType can ONLY be null if it's a stream (media resource) and |
369
|
|
|
// that stream is storing null as the content type |
370
|
|
|
throw new ODataException(Messages::unsupportedMediaType(), 415); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
$odataModelInstance = null; |
374
|
|
|
$hasResponseBody = true; |
375
|
|
|
// Execution required at this point if request points to any resource other than |
376
|
|
|
|
377
|
|
|
// (1) media resource - For Media resource 'getResponseContentType' already performed execution as |
378
|
|
|
// it needs to know the mime type of the stream |
379
|
|
|
// (2) metadata - internal resource |
380
|
|
|
// (3) service directory - internal resource |
381
|
|
|
if ($request->needExecution()) { |
382
|
|
|
$method = $this->getHost()->getOperationContext()->incomingRequest()->getMethod(); |
383
|
|
|
$uriProcessor->execute(); |
384
|
|
|
if (HTTPRequestMethod::DELETE() == $method) { |
385
|
|
|
$this->getHost()->setResponseStatusCode(HttpStatus::CODE_OK); |
386
|
|
|
return; |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
$objectModelSerializer = $this->getObjectSerialiser(); |
390
|
|
|
$objectModelSerializer->setRequest($request); |
391
|
|
|
|
392
|
|
|
$targetResourceType = $request->getTargetResourceType(); |
393
|
|
|
assert(null != $targetResourceType, "Target resource type cannot be null"); |
394
|
|
|
|
395
|
|
|
$method = (HTTPRequestMethod::POST() != $method); |
396
|
|
|
if (!$request->isSingleResult() && $method) { |
397
|
|
|
// Code path for collection (feed or links) |
398
|
|
|
$entryObjects = $request->getTargetResult(); |
399
|
|
|
assert(is_array($entryObjects), '!is_array($entryObjects)'); |
400
|
|
|
// If related resource set is empty for an entry then we should |
401
|
|
|
// not throw error instead response must be empty feed or empty links |
402
|
|
|
if ($request->isLinkUri()) { |
403
|
|
|
$odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects); |
404
|
|
|
assert( |
405
|
|
|
$odataModelInstance instanceof ODataURLCollection, |
406
|
|
|
'!$odataModelInstance instanceof ODataURLCollection' |
407
|
|
|
); |
408
|
|
|
} else { |
409
|
|
|
$odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects); |
410
|
|
|
assert($odataModelInstance instanceof ODataFeed, '!$odataModelInstance instanceof ODataFeed'); |
411
|
|
|
} |
412
|
|
|
} else { |
413
|
|
|
// Code path for entry, complex, bag, resource reference link, |
414
|
|
|
// primitive type or primitive value |
415
|
|
|
$result = $request->getTargetResult(); |
416
|
|
|
$requestTargetKind = $request->getTargetKind(); |
417
|
|
|
if ($request->isLinkUri()) { |
418
|
|
|
// In the query 'Orders(1245)/$links/Customer', the targeted |
419
|
|
|
// Customer might be null |
420
|
|
|
if (is_null($result)) { |
421
|
|
|
throw ODataException::createResourceNotFoundError($request->getIdentifier()); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
$odataModelInstance = $objectModelSerializer->writeUrlElement($result); |
425
|
|
|
} elseif (TargetKind::RESOURCE() == $requestTargetKind) { |
426
|
|
|
if (!is_null($this->getHost()->getRequestIfMatch()) |
427
|
|
|
&& !is_null($this->getHost()->getRequestIfNoneMatch()) |
428
|
|
|
) { |
429
|
|
|
throw ODataException::createBadRequestError( |
430
|
|
|
Messages::bothIfMatchAndIfNoneMatchHeaderSpecified() |
431
|
|
|
); |
432
|
|
|
} |
433
|
|
|
// handle entry resource |
434
|
|
|
$needToSerializeResponse = true; |
435
|
|
|
$eTag = $this->compareETag($result, $targetResourceType, $needToSerializeResponse); |
436
|
|
|
|
437
|
|
|
if ($needToSerializeResponse) { |
438
|
|
|
if (is_null($result)) { |
439
|
|
|
// In the query 'Orders(1245)/Customer', the targeted |
440
|
|
|
// Customer might be null |
441
|
|
|
// set status code to 204 => 'No Content' |
442
|
|
|
$this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT); |
443
|
|
|
$hasResponseBody = false; |
444
|
|
|
} else { |
445
|
|
|
$odataModelInstance = $objectModelSerializer->writeTopLevelElement($result); |
446
|
|
|
} |
447
|
|
|
} else { |
448
|
|
|
// Resource is not modified so set status code |
449
|
|
|
// to 304 => 'Not Modified' |
|
|
|
|
450
|
|
|
$this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED); |
451
|
|
|
$hasResponseBody = false; |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
// if resource has eTagProperty then eTag header needs to written |
455
|
|
|
if (!is_null($eTag)) { |
456
|
|
|
$this->getHost()->setResponseETag($eTag); |
457
|
|
|
} |
458
|
|
|
} elseif (TargetKind::COMPLEX_OBJECT() == $requestTargetKind) { |
459
|
|
|
$odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject( |
460
|
|
|
$result, |
461
|
|
|
$request->getProjectedProperty()->getName(), |
462
|
|
|
$targetResourceType |
463
|
|
|
); |
464
|
|
|
} elseif (TargetKind::BAG() == $requestTargetKind) { |
465
|
|
|
$odataModelInstance = $objectModelSerializer->writeTopLevelBagObject( |
466
|
|
|
$result, |
467
|
|
|
$request->getProjectedProperty()->getName(), |
468
|
|
|
$targetResourceType, |
469
|
|
|
$odataModelInstance |
|
|
|
|
470
|
|
|
); |
471
|
|
|
} elseif (TargetKind::PRIMITIVE() == $requestTargetKind) { |
472
|
|
|
$odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive( |
473
|
|
|
$result, |
474
|
|
|
$request->getProjectedProperty(), |
|
|
|
|
475
|
|
|
$odataModelInstance |
|
|
|
|
476
|
|
|
); |
477
|
|
|
} elseif (TargetKind::PRIMITIVE_VALUE() == $requestTargetKind) { |
|
|
|
|
478
|
|
|
// Code path for primitive value (Since its primitve no need for |
479
|
|
|
// object model serialization) |
480
|
|
|
// Customers('ANU')/CompanyName/$value => string |
|
|
|
|
481
|
|
|
// Employees(1)/Photo/$value => binary stream |
482
|
|
|
// Customers/$count => string |
483
|
|
|
} else { |
484
|
|
|
assert(false, 'Unexpected resource target kind'); |
485
|
|
|
} |
486
|
|
|
} |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
//Note: Response content type can be null for named stream |
490
|
|
|
if ($hasResponseBody && !is_null($responseContentType)) { |
491
|
|
|
if (TargetKind::MEDIA_RESOURCE() != $request->getTargetKind() |
492
|
|
|
&& MimeTypes::MIME_APPLICATION_OCTETSTREAM != $responseContentType) { |
493
|
|
|
//append charset for everything except: |
494
|
|
|
//stream resources as they have their own content type |
495
|
|
|
//binary properties (they content type will be App Octet for those...is this a good way? |
496
|
|
|
//we could also decide based upon the projected property |
497
|
|
|
|
498
|
|
|
$responseContentType .= ';charset=utf-8'; |
499
|
|
|
} |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
if ($hasResponseBody) { |
503
|
|
|
ResponseWriter::write($this, $request, $odataModelInstance, $responseContentType); |
504
|
|
|
} |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
/** |
508
|
|
|
* Gets the response format for the requested resource. |
509
|
|
|
* |
510
|
|
|
* @param RequestDescription $request The request submitted by client and it's execution result |
511
|
|
|
* @param UriProcessor $uriProcessor The reference to the UriProcessor |
512
|
|
|
* |
513
|
|
|
* @return string the response content-type, a null value means the requested resource |
514
|
|
|
* is named stream and IDSSP2::getStreamContentType returned null |
515
|
|
|
* |
516
|
|
|
* @throws ODataException, HttpHeaderFailure |
517
|
|
|
*/ |
518
|
|
|
public function getResponseContentType( |
519
|
|
|
RequestDescription $request, |
520
|
|
|
UriProcessor $uriProcessor |
521
|
|
|
) { |
522
|
|
|
|
523
|
|
|
$baseMimeTypes = [ |
524
|
|
|
MimeTypes::MIME_APPLICATION_JSON, |
525
|
|
|
MimeTypes::MIME_APPLICATION_JSON_FULL_META, |
526
|
|
|
MimeTypes::MIME_APPLICATION_JSON_NO_META, |
527
|
|
|
MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META, |
528
|
|
|
MimeTypes::MIME_APPLICATION_JSON_VERBOSE]; |
529
|
|
|
|
530
|
|
|
|
531
|
|
|
// The Accept request-header field specifies media types which are acceptable for the response |
532
|
|
|
|
533
|
|
|
$host = $this->getHost(); |
534
|
|
|
$requestAcceptText = $host->getRequestAccept(); |
535
|
|
|
$requestVersion = $request->getResponseVersion(); |
536
|
|
|
|
537
|
|
|
//if the $format header is present it overrides the accepts header |
538
|
|
|
$format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT); |
539
|
|
|
if (!is_null($format)) { |
540
|
|
|
//There's a strange edge case..if application/json is supplied and it's V3 |
541
|
|
|
if (MimeTypes::MIME_APPLICATION_JSON == $format && Version::v3() == $requestVersion) { |
542
|
|
|
//then it's actual minimalmetadata |
543
|
|
|
//TODO: should this be done with the header text too? |
544
|
|
|
$format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META; |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
$requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format); |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
//The response format can be dictated by the target resource kind. IE a $value will be different then expected |
551
|
|
|
//getTargetKind doesn't deal with link resources directly and this can change things |
552
|
|
|
$targetKind = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind(); |
553
|
|
|
|
554
|
|
|
switch ($targetKind) { |
555
|
|
|
case TargetKind::METADATA(): |
556
|
|
|
return HttpProcessUtility::selectMimeType( |
557
|
|
|
$requestAcceptText, |
558
|
|
|
array(MimeTypes::MIME_APPLICATION_XML) |
559
|
|
|
); |
560
|
|
|
|
561
|
|
View Code Duplication |
case TargetKind::SERVICE_DIRECTORY(): |
|
|
|
|
562
|
|
|
return HttpProcessUtility::selectMimeType( |
563
|
|
|
$requestAcceptText, |
564
|
|
|
array_merge( |
565
|
|
|
[MimeTypes::MIME_APPLICATION_ATOMSERVICE, |
566
|
|
|
MimeTypes::MIME_APPLICATION_XML], |
567
|
|
|
$baseMimeTypes |
568
|
|
|
) |
569
|
|
|
); |
570
|
|
|
|
571
|
|
|
case TargetKind::PRIMITIVE_VALUE(): |
572
|
|
|
$supportedResponseMimeTypes = array(MimeTypes::MIME_TEXTPLAIN); |
573
|
|
|
|
574
|
|
|
if ('$count' != $request->getIdentifier()) { |
575
|
|
|
$projectedProperty = $request->getProjectedProperty(); |
576
|
|
|
assert(!is_null($projectedProperty), 'is_null($projectedProperty)'); |
577
|
|
|
$type = $projectedProperty->getInstanceType(); |
578
|
|
|
assert($type instanceof IType, '!$type instanceof IType'); |
579
|
|
|
if ($type instanceof Binary) { |
580
|
|
|
$supportedResponseMimeTypes = array(MimeTypes::MIME_APPLICATION_OCTETSTREAM); |
581
|
|
|
} |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
return HttpProcessUtility::selectMimeType( |
585
|
|
|
$requestAcceptText, |
586
|
|
|
$supportedResponseMimeTypes |
587
|
|
|
); |
588
|
|
|
|
589
|
|
|
case TargetKind::PRIMITIVE(): |
590
|
|
|
case TargetKind::COMPLEX_OBJECT(): |
591
|
|
|
case TargetKind::BAG(): |
592
|
|
View Code Duplication |
case TargetKind::LINK(): |
|
|
|
|
593
|
|
|
return HttpProcessUtility::selectMimeType( |
594
|
|
|
$requestAcceptText, |
595
|
|
|
array_merge( |
596
|
|
|
[MimeTypes::MIME_APPLICATION_XML, |
597
|
|
|
MimeTypes::MIME_TEXTXML], |
598
|
|
|
$baseMimeTypes |
599
|
|
|
) |
600
|
|
|
); |
601
|
|
|
|
602
|
|
|
case TargetKind::RESOURCE(): |
603
|
|
|
return HttpProcessUtility::selectMimeType( |
604
|
|
|
$requestAcceptText, |
605
|
|
|
array_merge( |
606
|
|
|
[MimeTypes::MIME_APPLICATION_ATOM], |
607
|
|
|
$baseMimeTypes |
608
|
|
|
) |
609
|
|
|
); |
610
|
|
|
|
611
|
|
|
case TargetKind::MEDIA_RESOURCE(): |
612
|
|
|
if (!$request->isNamedStream() && !$request->getTargetResourceType()->isMediaLinkEntry()) { |
613
|
|
|
throw ODataException::createBadRequestError( |
614
|
|
|
Messages::badRequestInvalidUriForMediaResource( |
615
|
|
|
$host->getAbsoluteRequestUri()->getUrlAsString() |
616
|
|
|
) |
617
|
|
|
); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
$uriProcessor->execute(); |
621
|
|
|
$request->setExecuted(); |
622
|
|
|
// DSSW::getStreamContentType can throw error in 2 cases |
623
|
|
|
// 1. If the required stream implementation not found |
624
|
|
|
// 2. If IDSSP::getStreamContentType returns NULL for MLE |
625
|
|
|
$responseContentType = $this->getStreamProviderWrapper() |
626
|
|
|
->getStreamContentType( |
627
|
|
|
$request->getTargetResult(), |
628
|
|
|
$request->getResourceStreamInfo() |
|
|
|
|
629
|
|
|
); |
630
|
|
|
|
631
|
|
|
// Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not |
632
|
|
|
// yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL |
633
|
|
|
// then StreamWrapper will throw error |
634
|
|
|
if (!is_null($responseContentType)) { |
635
|
|
|
$responseContentType = HttpProcessUtility::selectMimeType( |
636
|
|
|
$requestAcceptText, |
637
|
|
|
array($responseContentType) |
638
|
|
|
); |
639
|
|
|
} |
640
|
|
|
|
641
|
|
|
return $responseContentType; |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
//If we got here, we just don't know what it is... |
645
|
|
|
throw new ODataException(Messages::unsupportedMediaType(), 415); |
646
|
|
|
} |
647
|
|
|
|
648
|
|
|
/** |
649
|
|
|
* For the given entry object compare its eTag (if it has eTag properties) |
650
|
|
|
* with current eTag request headers (if present). |
651
|
|
|
* |
652
|
|
|
* @param mixed &$entryObject entity resource for which etag |
653
|
|
|
* needs to be checked |
654
|
|
|
* @param ResourceType &$resourceType Resource type of the entry |
655
|
|
|
* object |
656
|
|
|
* @param bool &$needToSerializeResponse On return, this will contain |
657
|
|
|
* True if response needs to be |
658
|
|
|
* serialized, False otherwise |
659
|
|
|
* @param bool $needToSerializeResponse |
660
|
|
|
* |
661
|
|
|
* @return string|null The ETag for the entry object if it has eTag properties |
662
|
|
|
* NULL otherwise |
663
|
|
|
*/ |
664
|
|
|
protected function compareETag( |
665
|
|
|
&$entryObject, |
666
|
|
|
ResourceType & $resourceType, |
667
|
|
|
&$needToSerializeResponse |
668
|
|
|
) { |
669
|
|
|
$needToSerializeResponse = true; |
670
|
|
|
$eTag = null; |
671
|
|
|
$ifMatch = $this->getHost()->getRequestIfMatch(); |
672
|
|
|
$ifNoneMatch = $this->getHost()->getRequestIfNoneMatch(); |
673
|
|
|
if (is_null($entryObject)) { |
674
|
|
|
if (!is_null($ifMatch)) { |
675
|
|
|
throw ODataException::createPreConditionFailedError( |
676
|
|
|
Messages::eTagNotAllowedForNonExistingResource() |
677
|
|
|
); |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
return null; |
681
|
|
|
} |
682
|
|
|
|
683
|
|
|
if ($this->getConfiguration()->getValidateETagHeader() && !$resourceType->hasETagProperties()) { |
684
|
|
|
if (!is_null($ifMatch) || !is_null($ifNoneMatch)) { |
685
|
|
|
// No eTag properties but request has eTag headers, bad request |
686
|
|
|
throw ODataException::createBadRequestError( |
687
|
|
|
Messages::noETagPropertiesForType() |
688
|
|
|
); |
689
|
|
|
} |
690
|
|
|
|
691
|
|
|
// We need write the response but no eTag header |
692
|
|
|
return null; |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
if (!$this->getConfiguration()->getValidateETagHeader()) { |
696
|
|
|
// Configuration says do not validate ETag, so we will not write ETag header in the |
697
|
|
|
// response even though the requested resource support it |
698
|
|
|
return null; |
699
|
|
|
} |
700
|
|
|
|
701
|
|
|
if (is_null($ifMatch) && is_null($ifNoneMatch)) { |
|
|
|
|
702
|
|
|
// No request eTag header, we need to write the response |
703
|
|
|
// and eTag header |
704
|
|
|
} elseif (0 === strcmp($ifMatch, '*')) { |
|
|
|
|
705
|
|
|
// If-Match:* => we need to write the response and eTag header |
706
|
|
|
} elseif (0 === strcmp($ifNoneMatch, '*')) { |
707
|
|
|
// if-None-Match:* => Do not write the response (304 not modified), |
708
|
|
|
// but write eTag header |
709
|
|
|
$needToSerializeResponse = false; |
710
|
|
|
} else { |
711
|
|
|
$eTag = $this->getETagForEntry($entryObject, $resourceType); |
712
|
|
|
// Note: The following code for attaching the prefix W\" |
713
|
|
|
// and the suffix " can be done in getETagForEntry function |
714
|
|
|
// but that is causing an issue in Linux env where the |
715
|
|
|
// firefix browser is unable to parse the ETag in this case. |
716
|
|
|
// Need to follow up PHP core devs for this. |
717
|
|
|
$eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"'; |
718
|
|
|
if (!is_null($ifMatch)) { |
719
|
|
|
if (0 != strcmp($eTag, $ifMatch)) { |
720
|
|
|
// Requested If-Match value does not match with current |
721
|
|
|
// eTag Value then pre-condition error |
722
|
|
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html |
723
|
|
|
throw ODataException::createPreConditionFailedError( |
724
|
|
|
Messages::eTagValueDoesNotMatch() |
725
|
|
|
); |
726
|
|
|
} |
727
|
|
|
} elseif (0 === strcmp($eTag, $ifNoneMatch)) { |
728
|
|
|
//304 not modified, but in write eTag header |
729
|
|
|
$needToSerializeResponse = false; |
730
|
|
|
} |
731
|
|
|
} |
732
|
|
|
|
733
|
|
|
if (is_null($eTag)) { |
734
|
|
|
$eTag = $this->getETagForEntry($entryObject, $resourceType); |
735
|
|
|
// Note: The following code for attaching the prefix W\" |
736
|
|
|
// and the suffix " can be done in getETagForEntry function |
737
|
|
|
// but that is causing an issue in Linux env where the |
738
|
|
|
// firefix browser is unable to parse the ETag in this case. |
739
|
|
|
// Need to follow up PHP core devs for this. |
740
|
|
|
$eTag = ODataConstants::HTTP_WEAK_ETAG_PREFIX . $eTag . '"'; |
741
|
|
|
} |
742
|
|
|
|
743
|
|
|
return $eTag; |
744
|
|
|
} |
745
|
|
|
|
746
|
|
|
/** |
747
|
|
|
* Returns the etag for the given resource. |
748
|
|
|
* Note: This function will not add W\" prefix and " suffix, its callers |
749
|
|
|
* repsonsibility. |
750
|
|
|
* |
751
|
|
|
* @param mixed &$entryObject Resource for which etag value needs to |
752
|
|
|
* be returned |
753
|
|
|
* @param ResourceType &$resourceType Resource type of the $entryObject |
754
|
|
|
* |
755
|
|
|
* @return string|null ETag value for the given resource (with values encoded |
756
|
|
|
* for use in a URI) there are etag properties, NULL if |
757
|
|
|
* there is no etag property |
758
|
|
|
*/ |
759
|
|
|
protected function getETagForEntry(&$entryObject, ResourceType & $resourceType) |
760
|
|
|
{ |
761
|
|
|
$eTag = null; |
762
|
|
|
$comma = null; |
763
|
|
|
foreach ($resourceType->getETagProperties() as $eTagProperty) { |
764
|
|
|
$type = $eTagProperty->getInstanceType(); |
765
|
|
|
assert($type instanceof IType, '!$type instanceof IType'); |
766
|
|
|
|
767
|
|
|
$value = null; |
768
|
|
|
$property = $eTagProperty->getName(); |
769
|
|
|
try { |
770
|
|
|
//TODO #88...also this seems like dupe work |
771
|
|
|
$value = ReflectionHandler::getProperty($entryObject, $property); |
772
|
|
|
} catch (\ReflectionException $reflectionException) { |
773
|
|
|
throw ODataException::createInternalServerError( |
774
|
|
|
Messages::failedToAccessProperty($property, $resourceType->getName()) |
775
|
|
|
); |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
$eTagBase = $eTag . $comma; |
779
|
|
|
$eTag = $eTagBase . ((null == $value) ? 'null' : $type->convertToOData($value)); |
780
|
|
|
|
781
|
|
|
$comma = ','; |
782
|
|
|
} |
783
|
|
|
|
784
|
|
View Code Duplication |
if (!is_null($eTag)) { |
|
|
|
|
785
|
|
|
// If eTag is made up of datetime or string properties then the above |
786
|
|
|
// IType::convertToOData will perform utf8 and url encode. But we don't |
787
|
|
|
// want this for eTag value. |
788
|
|
|
$eTag = urldecode(utf8_decode($eTag)); |
789
|
|
|
|
790
|
|
|
return rtrim($eTag, ','); |
791
|
|
|
} |
792
|
|
|
|
793
|
|
|
return null; |
794
|
|
|
} |
795
|
|
|
|
796
|
|
|
/** |
797
|
|
|
* This function will perform the following operations: |
798
|
|
|
* (1) Invoke delegateRequestProcessing method to process the request based |
799
|
|
|
* on request method (GET, PUT/MERGE, POST, DELETE) |
800
|
|
|
* (3) If the result of processing of request needs to be serialized as HTTP |
801
|
|
|
* response body (e.g. GET request result in single resource or resource |
802
|
|
|
* collection, successful POST operation for an entity need inserted |
803
|
|
|
* entity to be serialized back etc..), Serialize the result by using |
804
|
|
|
* 'serializeReultForResponseBody' method |
805
|
|
|
* Set the serialized result to |
806
|
|
|
* WebOperationContext::Current()::OutgoingWebResponseContext::Stream. |
807
|
|
|
*/ |
808
|
|
|
protected function handleRequest2() |
809
|
|
|
{ |
810
|
|
|
} |
811
|
|
|
|
812
|
|
|
/** |
813
|
|
|
* This method will perform the following operations: |
814
|
|
|
* (1) If request method is GET, then result is already there in the |
815
|
|
|
* RequestDescription so simply return the RequestDescription |
816
|
|
|
* (2). If request method is for CDU |
817
|
|
|
* (Create/Delete/Update - POST/DELETE/PUT-MERGE) hand |
818
|
|
|
* over the responsibility to respective handlers. The handler |
819
|
|
|
* methods are: |
820
|
|
|
* (a) handlePOSTOperation() => POST |
821
|
|
|
* (b) handlePUTOperation() => PUT/MERGE |
822
|
|
|
* (c) handleDELETEOperation() => DELETE |
823
|
|
|
* (3). Check whether its required to write any result to the response |
824
|
|
|
* body |
825
|
|
|
* (a). Request method is GET |
826
|
|
|
* (b). Request is a POST for adding NEW Entry |
827
|
|
|
* (c). Request is a POST for adding Media Resource Stream |
828
|
|
|
* (d). Request is a POST for adding a link |
829
|
|
|
* (e). Request is a DELETE for deleting entry or relationship |
830
|
|
|
* (f). Request is a PUT/MERGE for updating an entry |
831
|
|
|
* (g). Request is a PUT for updating a link |
832
|
|
|
* In case a, b and c we need to write the result to response body, |
833
|
|
|
* for d, e, f and g no body content. |
834
|
|
|
* |
835
|
|
|
* @return RequestDescription|null Instance of RequestDescription with |
836
|
|
|
* result to be write back Null if no result to write |
837
|
|
|
*/ |
838
|
|
|
protected function delegateRequestProcessing() |
839
|
|
|
{ |
840
|
|
|
} |
841
|
|
|
|
842
|
|
|
/** |
843
|
|
|
* Serialize the result in the current request description using |
844
|
|
|
* appropriate odata writer (AtomODataWriter/JSONODataWriter). |
845
|
|
|
*/ |
846
|
|
|
protected function serializeResultForResponseBody() |
847
|
|
|
{ |
848
|
|
|
} |
849
|
|
|
|
850
|
|
|
/** |
851
|
|
|
* Handle POST request. |
852
|
|
|
* |
853
|
|
|
* |
854
|
|
|
* @throws NotImplementedException |
855
|
|
|
*/ |
856
|
|
|
protected function handlePOSTOperation() |
857
|
|
|
{ |
858
|
|
|
} |
859
|
|
|
|
860
|
|
|
/** |
861
|
|
|
* Handle PUT/MERGE request. |
862
|
|
|
* |
863
|
|
|
* |
864
|
|
|
* @throws NotImplementedException |
865
|
|
|
*/ |
866
|
|
|
protected function handlePUTOperation() |
867
|
|
|
{ |
868
|
|
|
} |
869
|
|
|
|
870
|
|
|
/** |
871
|
|
|
* Handle DELETE request. |
872
|
|
|
* |
873
|
|
|
* |
874
|
|
|
* @throws NotImplementedException |
875
|
|
|
*/ |
876
|
|
|
protected function handleDELETEOperation() |
877
|
|
|
{ |
878
|
|
|
} |
879
|
|
|
} |
880
|
|
|
|
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.