Total Complexity | 114 |
Total Lines | 889 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like BaseService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use BaseService, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
63 | abstract class BaseService implements IRequestHandler, IService |
||
64 | { |
||
65 | /** |
||
66 | * The wrapper over IStreamProvider implementation. |
||
67 | * |
||
68 | * @var StreamProviderWrapper |
||
69 | */ |
||
70 | protected $streamProvider; |
||
71 | /** |
||
72 | * To hold reference to ServiceConfiguration instance where the |
||
73 | * service specific rules (page limit, resource set access rights |
||
74 | * etc...) are defined. |
||
75 | * |
||
76 | * @var IServiceConfiguration |
||
77 | */ |
||
78 | protected $config; |
||
79 | /** |
||
80 | * Hold reference to object serialiser - bit wot turns PHP objects |
||
81 | * into message traffic on wire. |
||
82 | * |
||
83 | * @var IObjectSerialiser |
||
84 | */ |
||
85 | protected $objectSerialiser; |
||
86 | /** @var ODataWriterRegistry */ |
||
87 | protected $writerRegistry; |
||
88 | /** @var ODataReaderRegistry */ |
||
89 | protected $readerRegistry; |
||
90 | /** |
||
91 | * The wrapper over IQueryProvider and IMetadataProvider implementations. |
||
92 | * |
||
93 | * @var ProvidersWrapper |
||
94 | */ |
||
95 | private $providersWrapper; |
||
96 | /** |
||
97 | * Hold reference to the ServiceHost instance created by dispatcher, |
||
98 | * using this library can access headers and body of Http Request |
||
99 | * dispatcher received and the Http Response Dispatcher is going to send. |
||
100 | * |
||
101 | * @var ServiceHost |
||
102 | */ |
||
103 | private $serviceHost; |
||
104 | |||
105 | /** |
||
106 | * BaseService constructor. |
||
107 | * @param IObjectSerialiser|null $serialiser |
||
108 | * @param IMetadataProvider|null $metaProvider |
||
109 | * @param IServiceConfiguration|null $config |
||
110 | * @throws Exception |
||
111 | */ |
||
112 | protected function __construct( |
||
113 | IObjectSerialiser $serialiser = null, |
||
114 | IMetadataProvider $metaProvider = null, |
||
115 | IServiceConfiguration $config = null |
||
116 | ) { |
||
117 | if (null != $serialiser) { |
||
118 | $serialiser->setService($this); |
||
119 | } else { |
||
120 | $serialiser = new ObjectModelSerializer($this, null); |
||
121 | } |
||
122 | $this->config = $config ?? $this->initializeDefaultConfig(new ServiceConfiguration($metaProvider)); |
||
123 | $this->objectSerialiser = $serialiser; |
||
124 | } |
||
125 | |||
126 | //TODO: shouldn't we hide this from the interface..if we need it at all. |
||
127 | |||
128 | protected function initializeDefaultConfig(IServiceConfiguration $config) |
||
129 | { |
||
130 | return $config; |
||
131 | } |
||
132 | |||
133 | /** |
||
134 | * Sets the data service host instance. |
||
135 | * |
||
136 | * @param ServiceHost $serviceHost The data service host instance |
||
137 | */ |
||
138 | public function setHost(ServiceHost $serviceHost): void |
||
139 | { |
||
140 | $this->serviceHost = $serviceHost; |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * To get reference to operation context where we have direct access to |
||
145 | * headers and body of Http Request, we have received and the Http Response |
||
146 | * We are going to send. |
||
147 | * |
||
148 | * @return IOperationContext |
||
149 | */ |
||
150 | public function getOperationContext(): IOperationContext |
||
151 | { |
||
152 | return $this->getHost()->getOperationContext(); |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * Get reference to the data service host instance. |
||
157 | * |
||
158 | * @return ServiceHost |
||
159 | */ |
||
160 | public function getHost(): ServiceHost |
||
161 | { |
||
162 | assert(null != $this->serviceHost); |
||
163 | |||
164 | return $this->serviceHost; |
||
165 | } |
||
166 | |||
167 | /** |
||
168 | * Get reference to the wrapper over IStreamProvider or |
||
169 | * IStreamProvider2 implementations. |
||
170 | * |
||
171 | * @return StreamProviderWrapper |
||
172 | */ |
||
173 | public function getStreamProvider(): StreamProviderWrapper |
||
174 | { |
||
175 | if (null === $this->streamProvider) { |
||
176 | $this->streamProvider = new StreamProviderWrapper(); |
||
177 | $this->streamProvider->setService($this); |
||
178 | } |
||
179 | |||
180 | return $this->streamProvider; |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Top-level handler invoked by Dispatcher against any request to this |
||
185 | * service. This method will hand over request processing task to other |
||
186 | * functions which process the request, set required headers and Response |
||
187 | * stream (if any in Atom/Json format) in |
||
188 | * WebOperationContext::Current()::OutgoingWebResponseContext. |
||
189 | * Once this function returns, dispatcher uses global WebOperationContext |
||
190 | * to write out the request response to client. |
||
191 | * This function will perform the following operations: |
||
192 | * (1) Check whether the top level service class implements |
||
193 | * IServiceProvider which means the service is a custom service, in |
||
194 | * this case make sure the top level service class implements |
||
195 | * IMetaDataProvider and IQueryProvider. |
||
196 | * These are the minimal interfaces that a custom service to be |
||
197 | * implemented in order to expose its data as OData. Save reference to |
||
198 | * These interface implementations. |
||
199 | * NOTE: Here we will ensure only providers for IDSQP and IDSMP. The |
||
200 | * IDSSP will be ensured only when there is an GET request on MLE/Named |
||
201 | * stream. |
||
202 | * |
||
203 | * (2). Invoke 'Initialize' method of top level service for |
||
204 | * collecting the configuration rules set by the developer for this |
||
205 | * service. |
||
206 | * |
||
207 | * (3). Invoke the Uri processor to process the request URI. The uri |
||
208 | * processor will do the following: |
||
209 | * (a). Validate the request uri syntax using OData uri rules |
||
210 | * (b). Validate the request using metadata of this service |
||
211 | * (c). Parse the request uri and using, IQueryProvider |
||
212 | * implementation, fetches the resources pointed by the uri |
||
213 | * if required |
||
214 | * (d). Build a RequestDescription which encapsulate everything |
||
215 | * related to request uri (e.g. type of resource, result |
||
216 | * etc...) |
||
217 | * (3). Invoke handleRequest2 for further processing |
||
218 | * @throws ODataException |
||
219 | */ |
||
220 | public function handleRequest() |
||
221 | { |
||
222 | try { |
||
223 | $this->createProviders(); |
||
224 | $this->getHost()->validateQueryParameters(); |
||
225 | $uriProcessor = UriProcessorNew::process($this); |
||
226 | $request = $uriProcessor->getRequest(); |
||
227 | if (TargetKind::BATCH() == $request->getTargetKind()) { |
||
228 | //dd($request); |
||
229 | $this->getProvidersWrapper()->startTransaction(true); |
||
230 | try { |
||
231 | $this->handleBatchRequest($request); |
||
232 | } catch (Exception $ex) { |
||
233 | $this->getProvidersWrapper()->rollBackTransaction(); |
||
234 | throw $ex; |
||
235 | } |
||
236 | $this->getProvidersWrapper()->commitTransaction(); |
||
237 | } else { |
||
238 | $this->serializeResult($request, $uriProcessor); |
||
239 | } |
||
240 | } catch (Exception $exception) { |
||
241 | ErrorHandler::handleException($exception, $this); |
||
242 | // Return to dispatcher for writing serialized exception |
||
243 | return; |
||
244 | } |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * This method will query and validates for IMetadataProvider and IQueryProvider implementations, invokes |
||
249 | * BaseService::Initialize to initialize service specific policies. |
||
250 | * |
||
251 | * @throws ODataException |
||
252 | * @throws Exception |
||
253 | */ |
||
254 | protected function createProviders() |
||
255 | { |
||
256 | $metadataProvider = $this->getMetadataProvider(); |
||
257 | if (null === $metadataProvider) { |
||
258 | throw ODataException::createInternalServerError(Messages::providersWrapperNull()); |
||
259 | } |
||
260 | |||
261 | if (!$metadataProvider instanceof IMetadataProvider) { |
||
|
|||
262 | throw ODataException::createInternalServerError(Messages::invalidMetadataInstance()); |
||
263 | } |
||
264 | |||
265 | $queryProvider = $this->getQueryProvider(); |
||
266 | |||
267 | if (null === $queryProvider) { |
||
268 | throw ODataException::createInternalServerError(Messages::providersWrapperNull()); |
||
269 | } |
||
270 | |||
271 | $this->providersWrapper = new ProvidersWrapper( |
||
272 | $metadataProvider, |
||
273 | $queryProvider, |
||
274 | $this->config |
||
275 | ); |
||
276 | |||
277 | $this->initialize($this->config); |
||
278 | |||
279 | //TODO: this seems like a bad spot to do this |
||
280 | $this->writerRegistry = new ODataWriterRegistry(); |
||
281 | $this->readerRegistry = new ODataReaderRegistry(); |
||
282 | $this->registerWriters(); |
||
283 | $this->registerReaders(); |
||
284 | } |
||
285 | |||
286 | /** |
||
287 | * @return IMetadataProvider |
||
288 | */ |
||
289 | abstract public function getMetadataProvider(); |
||
290 | |||
291 | /** |
||
292 | * @return IQueryProvider|null |
||
293 | */ |
||
294 | abstract public function getQueryProvider(): ?IQueryProvider; |
||
295 | |||
296 | /** |
||
297 | * @throws Exception |
||
298 | */ |
||
299 | public function registerWriters() |
||
300 | { |
||
301 | $registry = $this->getODataWriterRegistry(); |
||
302 | $serviceVersion = $this->getConfiguration()->getMaxDataServiceVersion(); |
||
303 | $serviceURI = $this->getHost()->getAbsoluteServiceUri()->getUrlAsString(); |
||
304 | |||
305 | //We always register the v1 stuff |
||
306 | $registry->register( |
||
307 | new JsonODataV1Writer( |
||
308 | $this->getConfiguration()->getLineEndings(), |
||
309 | $this->getConfiguration()->getPrettyOutput() |
||
310 | ) |
||
311 | ); |
||
312 | $registry->register( |
||
313 | new AtomODataWriter( |
||
314 | $this->getConfiguration()->getLineEndings(), |
||
315 | $this->getConfiguration()->getPrettyOutput(), |
||
316 | $serviceURI |
||
317 | ) |
||
318 | ); |
||
319 | |||
320 | if (-1 < $serviceVersion->compare(Version::v2())) { |
||
321 | $registry->register( |
||
322 | new JsonODataV2Writer( |
||
323 | $this->getConfiguration()->getLineEndings(), |
||
324 | $this->getConfiguration()->getPrettyOutput() |
||
325 | ) |
||
326 | ); |
||
327 | } |
||
328 | |||
329 | if (-1 < $serviceVersion->compare(Version::v3())) { |
||
330 | $registry->register( |
||
331 | new JsonLightODataWriter( |
||
332 | $this->getConfiguration()->getLineEndings(), |
||
333 | $this->getConfiguration()->getPrettyOutput(), |
||
334 | JsonLightMetadataLevel::NONE(), |
||
335 | $serviceURI |
||
336 | ) |
||
337 | ); |
||
338 | $registry->register( |
||
339 | new JsonLightODataWriter( |
||
340 | $this->getConfiguration()->getLineEndings(), |
||
341 | $this->getConfiguration()->getPrettyOutput(), |
||
342 | JsonLightMetadataLevel::MINIMAL(), |
||
343 | $serviceURI |
||
344 | ) |
||
345 | ); |
||
346 | $registry->register( |
||
347 | new JsonLightODataWriter( |
||
348 | $this->getConfiguration()->getLineEndings(), |
||
349 | $this->getConfiguration()->getPrettyOutput(), |
||
350 | JsonLightMetadataLevel::FULL(), |
||
351 | $serviceURI |
||
352 | ) |
||
353 | ); |
||
354 | } |
||
355 | } |
||
356 | |||
357 | /** |
||
358 | * Returns the ODataWriterRegistry to use when writing the response to a service document or resource request. |
||
359 | * |
||
360 | * @return ODataWriterRegistry |
||
361 | */ |
||
362 | public function getODataWriterRegistry(): ODataWriterRegistry |
||
363 | { |
||
364 | assert(null != $this->writerRegistry); |
||
365 | |||
366 | return $this->writerRegistry; |
||
367 | } |
||
368 | |||
369 | /** |
||
370 | * Gets reference to ServiceConfiguration instance so that |
||
371 | * service specific rules defined by the developer can be |
||
372 | * accessed. |
||
373 | * |
||
374 | * @return IServiceConfiguration |
||
375 | */ |
||
376 | public function getConfiguration(): IServiceConfiguration |
||
381 | } |
||
382 | |||
383 | public function registerReaders() |
||
384 | { |
||
385 | $registry = $this->getODataReaderRegistry(); |
||
386 | //We always register the v1 stuff |
||
387 | $registry->register(new AtomODataReader()); |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * Returns the ODataReaderRegistry to use when writing the response to a service document or resource request. |
||
392 | * |
||
393 | * @return ODataReaderRegistry |
||
394 | */ |
||
395 | public function getODataReaderRegistry(): ODataReaderRegistry |
||
396 | { |
||
397 | assert(null != $this->writerRegistry); |
||
398 | |||
399 | return $this->readerRegistry; |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * Get the wrapper over developer's IQueryProvider and IMetadataProvider implementation. |
||
404 | * |
||
405 | * @return ProvidersWrapper |
||
406 | */ |
||
407 | public function getProvidersWrapper(): ProvidersWrapper |
||
408 | { |
||
409 | return $this->providersWrapper; |
||
410 | } |
||
411 | |||
412 | /** |
||
413 | * @param $request |
||
414 | * @throws ODataException |
||
415 | */ |
||
416 | private function handleBatchRequest($request) |
||
417 | { |
||
418 | $cloneThis = clone $this; |
||
419 | $batchProcessor = new BatchProcessor($cloneThis, $request); |
||
420 | $batchProcessor->handleBatch(); |
||
421 | $response = $batchProcessor->getResponse(); |
||
422 | $this->getHost()->setResponseStatusCode(HttpStatus::CODE_ACCEPTED); |
||
423 | $this->getHost()->setResponseContentType('multipart/mixed; boundary=' . $batchProcessor->getBoundary()); |
||
424 | // Hack: this needs to be sorted out in the future as we hookup other versions. |
||
425 | $this->getHost()->setResponseVersion('3.0;'); |
||
426 | $this->getHost()->setResponseCacheControl(ODataConstants::HTTPRESPONSE_HEADER_CACHECONTROL_NOCACHE); |
||
427 | $this->getHost()->getOperationContext()->outgoingResponse()->setStream($response); |
||
428 | } |
||
429 | |||
430 | //TODO: i don't want this to be public..but it's the only way to test it right now... |
||
431 | |||
432 | /** |
||
433 | * Serialize the requested resource. |
||
434 | * |
||
435 | * @param RequestDescription $request The description of the request submitted by the client |
||
436 | * @param IUriProcessor $uriProcessor Reference to the uri processor |
||
437 | * |
||
438 | * @throws Common\HttpHeaderFailure |
||
439 | * @throws Common\UrlFormatException |
||
440 | * @throws InvalidOperationException |
||
441 | * @throws ODataException |
||
442 | * @throws ReflectionException |
||
443 | * @throws Exception |
||
444 | */ |
||
445 | protected function serializeResult(RequestDescription $request, IUriProcessor $uriProcessor) |
||
446 | { |
||
447 | $isETagHeaderAllowed = $request->isETagHeaderAllowed(); |
||
448 | |||
449 | if ($this->getConfiguration()->getValidateETagHeader() && !$isETagHeaderAllowed) { |
||
450 | if (null !== $this->getHost()->getRequestIfMatch() |
||
451 | || null !== $this->getHost()->getRequestIfNoneMatch() |
||
452 | ) { |
||
453 | throw ODataException::createBadRequestError( |
||
454 | Messages::eTagCannotBeSpecified($this->getHost()->getAbsoluteRequestUri()->getUrlAsString()) |
||
455 | ); |
||
456 | } |
||
457 | } |
||
458 | |||
459 | $responseContentType = $this->getResponseContentType($request, $uriProcessor); |
||
460 | |||
461 | if (null === $responseContentType && $request->getTargetKind() != TargetKind::MEDIA_RESOURCE()) { |
||
462 | //the responseContentType can ONLY be null if it's a stream (media resource) and |
||
463 | // that stream is storing null as the content type |
||
464 | throw new ODataException(Messages::unsupportedMediaType(), 415); |
||
465 | } |
||
466 | |||
467 | $odataModelInstance = null; |
||
468 | $hasResponseBody = true; |
||
469 | // Execution required at this point if request points to any resource other than |
||
470 | |||
471 | // (1) media resource - For Media resource 'getResponseContentType' already performed execution as |
||
472 | // it needs to know the mime type of the stream |
||
473 | // (2) metadata - internal resource |
||
474 | // (3) service directory - internal resource |
||
475 | if ($request->needExecution()) { |
||
476 | $method = $this->getHost()->getOperationContext()->incomingRequest()->getMethod(); |
||
477 | $uriProcessor->execute(); |
||
478 | if (HTTPRequestMethod::DELETE() == $method) { |
||
479 | $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT); |
||
480 | |||
481 | return; |
||
482 | } |
||
483 | |||
484 | $objectModelSerializer = $this->getObjectSerialiser(); |
||
485 | $objectModelSerializer->setRequest($request); |
||
486 | |||
487 | $targetResourceType = $request->getTargetResourceType(); |
||
488 | if (null === $targetResourceType) { |
||
489 | throw new InvalidOperationException('Target resource type cannot be null'); |
||
490 | } |
||
491 | |||
492 | $methodIsNotPost = (HTTPRequestMethod::POST() != $method); |
||
493 | $methodIsNotDelete = (HTTPRequestMethod::DELETE() != $method); |
||
494 | if (!$request->isSingleResult() && $methodIsNotPost) { |
||
495 | // Code path for collection (feed or links) |
||
496 | $entryObjects = $request->getTargetResult(); |
||
497 | if (!$entryObjects instanceof QueryResult) { |
||
498 | throw new InvalidOperationException('!$entryObjects instanceof QueryResult'); |
||
499 | } |
||
500 | if (!is_array($entryObjects->results)) { |
||
501 | throw new InvalidOperationException('!is_array($entryObjects->results)'); |
||
502 | } |
||
503 | // If related resource set is empty for an entry then we should |
||
504 | // not throw error instead response must be empty feed or empty links |
||
505 | if ($request->isLinkUri()) { |
||
506 | $odataModelInstance = $objectModelSerializer->writeUrlElements($entryObjects); |
||
507 | if (!$odataModelInstance instanceof ODataURLCollection) { |
||
508 | throw new InvalidOperationException('!$odataModelInstance instanceof ODataURLCollection'); |
||
509 | } |
||
510 | } else { |
||
511 | $odataModelInstance = $objectModelSerializer->writeTopLevelElements($entryObjects); |
||
512 | if (!$odataModelInstance instanceof ODataFeed) { |
||
513 | throw new InvalidOperationException('!$odataModelInstance instanceof ODataFeed'); |
||
514 | } |
||
515 | } |
||
516 | } else { |
||
517 | // Code path for entity, complex, bag, resource reference link, |
||
518 | // primitive type or primitive value |
||
519 | $result = $request->getTargetResult(); |
||
520 | if (!$result instanceof QueryResult) { |
||
521 | $result = new QueryResult(); |
||
522 | $result->results = $request->getTargetResult(); |
||
523 | } |
||
524 | $requestTargetKind = $request->getTargetKind(); |
||
525 | $requestProperty = $request->getProjectedProperty(); |
||
526 | if ($request->isLinkUri()) { |
||
527 | // In the query 'Orders(1245)/$links/Customer', the targeted |
||
528 | // Customer might be null |
||
529 | if (null === $result->results && $methodIsNotPost && $methodIsNotDelete) { |
||
530 | throw ODataException::createResourceNotFoundError($request->getIdentifier()); |
||
531 | } |
||
532 | if ($methodIsNotPost && $methodIsNotDelete) { |
||
533 | $odataModelInstance = $objectModelSerializer->writeUrlElement($result); |
||
534 | } |
||
535 | } elseif (TargetKind::RESOURCE() == $requestTargetKind |
||
536 | || TargetKind::SINGLETON() == $requestTargetKind) { |
||
537 | if (null !== $this->getHost()->getRequestIfMatch() |
||
538 | && null !== $this->getHost()->getRequestIfNoneMatch() |
||
539 | ) { |
||
540 | throw ODataException::createBadRequestError( |
||
541 | Messages::bothIfMatchAndIfNoneMatchHeaderSpecified() |
||
542 | ); |
||
543 | } |
||
544 | // handle entry resource |
||
545 | $needToSerializeResponse = true; |
||
546 | $eTag = $this->compareETag( |
||
547 | $result, |
||
548 | $targetResourceType, |
||
549 | $needToSerializeResponse |
||
550 | ); |
||
551 | if ($needToSerializeResponse) { |
||
552 | if (null === $result || null === $result->results) { |
||
553 | // In the query 'Orders(1245)/Customer', the targeted |
||
554 | // Customer might be null |
||
555 | // set status code to 204 => 'No Content' |
||
556 | $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOCONTENT); |
||
557 | $hasResponseBody = false; |
||
558 | } else { |
||
559 | $odataModelInstance = $objectModelSerializer->writeTopLevelElement($result); |
||
560 | } |
||
561 | } else { |
||
562 | // Resource is not modified so set status code |
||
563 | // to 304 => 'Not Modified' |
||
564 | $this->getHost()->setResponseStatusCode(HttpStatus::CODE_NOT_MODIFIED); |
||
565 | $hasResponseBody = false; |
||
566 | } |
||
567 | |||
568 | // if resource has eTagProperty then eTag header needs to written |
||
569 | if (null !== $eTag) { |
||
570 | $this->getHost()->setResponseETag($eTag); |
||
571 | } |
||
572 | } elseif (TargetKind::COMPLEX_OBJECT() == $requestTargetKind) { |
||
573 | if (null === $requestProperty) { |
||
574 | throw new InvalidOperationException('Projected request property cannot be null'); |
||
575 | } |
||
576 | $odataModelInstance = $objectModelSerializer->writeTopLevelComplexObject( |
||
577 | $result, |
||
578 | $requestProperty->getName(), |
||
579 | $targetResourceType |
||
580 | ); |
||
581 | } elseif (TargetKind::BAG() == $requestTargetKind) { |
||
582 | if (null === $requestProperty) { |
||
583 | throw new InvalidOperationException('Projected request property cannot be null'); |
||
584 | } |
||
585 | $odataModelInstance = $objectModelSerializer->writeTopLevelBagObject( |
||
586 | $result, |
||
587 | $requestProperty->getName(), |
||
588 | $targetResourceType |
||
589 | ); |
||
590 | } elseif (TargetKind::PRIMITIVE() == $requestTargetKind) { |
||
591 | $odataModelInstance = $objectModelSerializer->writeTopLevelPrimitive( |
||
592 | $result, |
||
593 | $requestProperty |
||
594 | ); |
||
595 | } elseif (TargetKind::PRIMITIVE_VALUE() == $requestTargetKind) { |
||
596 | // Code path for primitive value (Since its primitive no need for |
||
597 | // object model serialization) |
||
598 | // Customers('ANU')/CompanyName/$value => string |
||
599 | // Employees(1)/Photo/$value => binary stream |
||
600 | // Customers/$count => string |
||
601 | } else { |
||
602 | throw new InvalidOperationException('Unexpected resource target kind'); |
||
603 | } |
||
604 | } |
||
605 | } |
||
606 | |||
607 | //Note: Response content type can be null for named stream |
||
608 | if ($hasResponseBody && null !== $responseContentType) { |
||
609 | if (TargetKind::MEDIA_RESOURCE() != $request->getTargetKind() |
||
610 | && MimeTypes::MIME_APPLICATION_OCTETSTREAM != $responseContentType) { |
||
611 | //append charset for everything except: |
||
612 | //stream resources as they have their own content type |
||
613 | //binary properties (they content type will be App Octet for those...is this a good way? |
||
614 | //we could also decide based upon the projected property |
||
615 | |||
616 | $responseContentType .= ';charset=utf-8'; |
||
617 | } |
||
618 | } |
||
619 | |||
620 | if ($hasResponseBody) { |
||
621 | ResponseWriter::write($this, $request, $odataModelInstance, $responseContentType); |
||
622 | } |
||
623 | } |
||
624 | |||
625 | /** |
||
626 | * Gets the response format for the requested resource. |
||
627 | * |
||
628 | * @param RequestDescription $request The request submitted by client and it's execution result |
||
629 | * @param IUriProcessor $uriProcessor The reference to the IUriProcessor |
||
630 | * |
||
631 | * @throws Common\HttpHeaderFailure |
||
632 | * @throws InvalidOperationException |
||
633 | * @throws ODataException , HttpHeaderFailure |
||
634 | * @throws ReflectionException |
||
635 | * @throws Common\UrlFormatException |
||
636 | * @return string|null the response content-type, a null value means the requested resource |
||
637 | * is named stream and IDSSP2::getStreamContentType returned null |
||
638 | */ |
||
639 | public function getResponseContentType( |
||
640 | RequestDescription $request, |
||
641 | IUriProcessor $uriProcessor |
||
642 | ): ?string { |
||
643 | $baseMimeTypes = [ |
||
644 | MimeTypes::MIME_APPLICATION_JSON, |
||
645 | MimeTypes::MIME_APPLICATION_JSON_FULL_META, |
||
646 | MimeTypes::MIME_APPLICATION_JSON_NO_META, |
||
647 | MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META, |
||
648 | MimeTypes::MIME_APPLICATION_JSON_VERBOSE,]; |
||
649 | |||
650 | // The Accept request-header field specifies media types which are acceptable for the response |
||
651 | |||
652 | $host = $this->getHost(); |
||
653 | $requestAcceptText = $host->getRequestAccept(); |
||
654 | $requestVersion = $request->getResponseVersion(); |
||
655 | |||
656 | //if the $format header is present it overrides the accepts header |
||
657 | $format = $host->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FORMAT); |
||
658 | if (null !== $format) { |
||
659 | //There's a strange edge case..if application/json is supplied and it's V3 |
||
660 | if (MimeTypes::MIME_APPLICATION_JSON == $format && Version::v3() == $requestVersion) { |
||
661 | //then it's actual minimalmetadata |
||
662 | //TODO: should this be done with the header text too? |
||
663 | $format = MimeTypes::MIME_APPLICATION_JSON_MINIMAL_META; |
||
664 | } |
||
665 | |||
666 | $requestAcceptText = ServiceHost::translateFormatToMime($requestVersion, $format); |
||
667 | } |
||
668 | |||
669 | //The response format can be dictated by the target resource kind. IE a $value will be different then expected |
||
670 | //getTargetKind doesn't deal with link resources directly and this can change things |
||
671 | $targetKind = $request->isLinkUri() ? TargetKind::LINK() : $request->getTargetKind(); |
||
672 | |||
673 | switch ($targetKind) { |
||
674 | case TargetKind::METADATA(): |
||
675 | return HttpProcessUtility::selectMimeType( |
||
676 | $requestAcceptText, |
||
677 | [MimeTypes::MIME_APPLICATION_XML] |
||
678 | ); |
||
679 | |||
680 | case TargetKind::SERVICE_DIRECTORY(): |
||
681 | return HttpProcessUtility::selectMimeType( |
||
682 | $requestAcceptText, |
||
683 | array_merge( |
||
684 | [MimeTypes::MIME_APPLICATION_ATOMSERVICE], |
||
685 | $baseMimeTypes |
||
686 | ) |
||
687 | ); |
||
688 | |||
689 | case TargetKind::PRIMITIVE_VALUE(): |
||
690 | $supportedResponseMimeTypes = [MimeTypes::MIME_TEXTPLAIN]; |
||
691 | |||
692 | if ('$count' != $request->getIdentifier()) { |
||
693 | $projectedProperty = $request->getProjectedProperty(); |
||
694 | if (null === $projectedProperty) { |
||
695 | throw new InvalidOperationException('is_null($projectedProperty)'); |
||
696 | } |
||
697 | $type = $projectedProperty->getInstanceType(); |
||
698 | if (!$type instanceof IType) { |
||
699 | throw new InvalidOperationException('!$type instanceof IType'); |
||
700 | } |
||
701 | if ($type instanceof Binary) { |
||
702 | $supportedResponseMimeTypes = [MimeTypes::MIME_APPLICATION_OCTETSTREAM]; |
||
703 | } |
||
704 | } |
||
705 | |||
706 | return HttpProcessUtility::selectMimeType( |
||
707 | $requestAcceptText, |
||
708 | $supportedResponseMimeTypes |
||
709 | ); |
||
710 | |||
711 | case TargetKind::PRIMITIVE(): |
||
712 | case TargetKind::COMPLEX_OBJECT(): |
||
713 | case TargetKind::BAG(): |
||
714 | case TargetKind::LINK(): |
||
715 | return HttpProcessUtility::selectMimeType( |
||
716 | $requestAcceptText, |
||
717 | array_merge( |
||
718 | [MimeTypes::MIME_APPLICATION_XML, |
||
719 | MimeTypes::MIME_TEXTXML,], |
||
720 | $baseMimeTypes |
||
721 | ) |
||
722 | ); |
||
723 | |||
724 | case TargetKind::SINGLETON(): |
||
725 | case TargetKind::RESOURCE(): |
||
726 | return HttpProcessUtility::selectMimeType( |
||
727 | $requestAcceptText, |
||
728 | array_merge( |
||
729 | [MimeTypes::MIME_APPLICATION_ATOM], |
||
730 | $baseMimeTypes |
||
731 | ) |
||
732 | ); |
||
733 | |||
734 | case TargetKind::MEDIA_RESOURCE(): |
||
735 | if (!$request->isNamedStream() && true !== $request->getTargetResourceType()->isMediaLinkEntry()) { |
||
736 | throw ODataException::createBadRequestError( |
||
737 | Messages::badRequestInvalidUriForMediaResource( |
||
738 | $host->getAbsoluteRequestUri()->getUrlAsString() |
||
739 | ) |
||
740 | ); |
||
741 | } |
||
742 | |||
743 | $uriProcessor->execute(); |
||
744 | $request->setExecuted(); |
||
745 | // DSSW::getStreamContentType can throw error in 2 cases |
||
746 | // 1. If the required stream implementation not found |
||
747 | // 2. If IDSSP::getStreamContentType returns NULL for MLE |
||
748 | $responseContentType = $this->getStreamProviderWrapper() |
||
749 | ->getStreamContentType( |
||
750 | $request->getTargetResult(), |
||
751 | $request->getResourceStreamInfo() |
||
752 | ); |
||
753 | |||
754 | // Note StreamWrapper::getStreamContentType can return NULL if the requested named stream has not |
||
755 | // yet been uploaded. But for an MLE if IDSSP::getStreamContentType returns NULL |
||
756 | // then StreamWrapper will throw error |
||
757 | if (null !== $responseContentType) { |
||
758 | $responseContentType = HttpProcessUtility::selectMimeType( |
||
759 | $requestAcceptText, |
||
760 | [$responseContentType] |
||
761 | ); |
||
762 | } |
||
763 | |||
764 | return $responseContentType; |
||
765 | } |
||
766 | |||
767 | //If we got here, we just don't know what it is... |
||
768 | throw new ODataException(Messages::unsupportedMediaType(), 415); |
||
769 | } |
||
770 | |||
771 | /** |
||
772 | * Gets reference to wrapper class instance over IDSSP implementation. |
||
773 | * |
||
774 | * @return StreamProviderWrapper |
||
775 | */ |
||
776 | public function getStreamProviderWrapper() |
||
779 | } |
||
780 | |||
781 | /** |
||
782 | * Get reference to object serialiser - bit wot turns PHP objects |
||
783 | * into message traffic on wire. |
||
784 | * |
||
785 | * @return IObjectSerialiser |
||
786 | */ |
||
787 | public function getObjectSerialiser(): IObjectSerialiser |
||
788 | { |
||
789 | assert(null != $this->objectSerialiser); |
||
790 | |||
791 | return $this->objectSerialiser; |
||
792 | } |
||
793 | |||
794 | /** |
||
795 | * For the given entry object compare its eTag (if it has eTag properties) |
||
796 | * with current eTag request headers (if present). |
||
797 | * |
||
798 | * @param mixed &$entryObject entity resource for which etag |
||
799 | * needs to be checked |
||
800 | * @param ResourceType &$resourceType Resource type of the entry |
||
801 | * object |
||
802 | * @param bool &$needToSerializeResponse On return, this will contain |
||
803 | * True if response needs to be |
||
804 | * serialized, False otherwise |
||
805 | * |
||
806 | * @throws InvalidOperationException |
||
807 | * @throws ReflectionException |
||
808 | * @throws ODataException |
||
809 | * @return string|null The ETag for the entry object if it has eTag properties |
||
810 | * NULL otherwise |
||
811 | */ |
||
812 | protected function compareETag( |
||
892 | } |
||
893 | |||
894 | /** |
||
895 | * Returns the etag for the given resource. |
||
896 | * Note: This function will not add W\" prefix and " suffix, that is caller's |
||
897 | * responsibility. |
||
898 | * |
||
899 | * @param mixed &$entryObject Resource for which etag value needs to |
||
900 | * be returned |
||
901 | * @param ResourceType &$resourceType Resource type of the $entryObject |
||
902 | * |
||
903 | * @throws InvalidOperationException |
||
904 | * @throws ReflectionException |
||
905 | * @throws ODataException |
||
906 | * @return string|null ETag value for the given resource (with values encoded |
||
907 | * for use in a URI) there are etag properties, NULL if |
||
908 | * there is no etag property |
||
909 | */ |
||
910 | protected function getETagForEntry(&$entryObject, ResourceType &$resourceType): ?string |
||
946 | } |
||
947 | |||
948 | /** |
||
949 | * @return IStreamProvider2 |
||
950 | */ |
||
951 | abstract public function getStreamProviderX(); |
||
952 | } |
||
953 |