PageRenderTime 47ms CodeModel.GetById 2ms app.highlight 35ms RepoModel.GetById 2ms app.codeStats 0ms

/OData Producer for PHP/library/ODataProducer/UriProcessor/QueryProcessor/QueryProcessor.php

#
PHP | 670 lines | 392 code | 44 blank | 234 comment | 68 complexity | 0ad659886f493fbd76f1a87d54613a70 MD5 | raw file
  1<?php
  2/** 
  3 * Processor to process the query options of the request uri.
  4 * 
  5 * PHP version 5.3
  6 * 
  7 * @category  ODataProducer
  8 * @package   ODataProducer_UriProcessor_QueryProcessor
  9 * @author    Anu T Chandy <odataphpproducer_alias@microsoft.com>
 10 * @copyright 2011 Microsoft Corp. (http://www.microsoft.com)
 11 * @license   New BSD license, (http://www.opensource.org/licenses/bsd-license.php)
 12 * @version   SVN: 1.0
 13 * @link      http://odataphpproducer.codeplex.com
 14 * 
 15 */
 16namespace ODataProducer\UriProcessor\QueryProcessor;
 17use ODataProducer\Providers\Metadata\Type\Int32;
 18use ODataProducer\Providers\Metadata\ResourceTypeKind;
 19use ODataProducer\UriProcessor\RequestCountOption;
 20use ODataProducer\UriProcessor\RequestDescription;
 21use ODataProducer\UriProcessor\ResourcePathProcessor\SegmentParser\RequestTargetKind;
 22use ODataProducer\UriProcessor\ResourcePathProcessor\SegmentParser\RequestTargetSource;
 23use ODataProducer\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenParser;
 24use ODataProducer\UriProcessor\QueryProcessor\OrderByParser\OrderByParser;
 25use ODataProducer\UriProcessor\QueryProcessor\ExpressionParser\ExpressionParser2;
 26use ODataProducer\UriProcessor\QueryProcessor\ExpressionParser\InternalFilterInfo;
 27use ODataProducer\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandProjectionParser;
 28use ODataProducer\Common\Messages;
 29use ODataProducer\Common\ODataException;
 30use ODataProducer\Common\ODataConstants;
 31use ODataProducer\DataService;
 32/**
 33 * OData query options processor.
 34 * 
 35 * @category  ODataProducer
 36 * @package   ODataProducer_UriProcessor_QueryProcessor
 37 * @author    Anu T Chandy <odataphpproducer_alias@microsoft.com>
 38 * @copyright 2011 Microsoft Corp. (http://www.microsoft.com)
 39 * @license   New BSD license, (http://www.opensource.org/licenses/bsd-license.php)
 40 * @version   Release: 1.0
 41 * @link      http://odataphpproducer.codeplex.com
 42 */
 43class QueryProcessor
 44{
 45    /**
 46     * Holds details of the request that client has submitted.
 47     * 
 48     * @var RequestDescription
 49     */
 50    private $_requestDescription;
 51
 52    /**
 53     * Holds reference to the underlying data service specific
 54     * instance.  
 55     * 
 56     * @var DataService
 57     */
 58    private $_dataService;
 59
 60    /**
 61     * Whether the $orderby, $skip, $take and $count options can be 
 62     * applied to the request.
 63     * 
 64     * @var boolean
 65     */
 66    private $_setQueryApplicable;
 67
 68    /**
 69     * Whether the top level request is a candidate for paging
 70     * 
 71     * @var boolean
 72     */
 73    private $_pagingApplicable;
 74
 75    /**
 76     * Whether $expand, $select can be applied to the request.
 77     * 
 78     * @var boolean
 79     */
 80    private $_expandSelectApplicable;
 81
 82    /**
 83     * Creates new instance of QueryProcessor
 84     * 
 85     * @param RequestDescription &$requestDescription Description of the request 
 86     *                                                submitted by client.
 87     * @param DataService        &$dataService        Reference to the data service.
 88     */
 89    private function __construct(RequestDescription &$requestDescription, 
 90        DataService &$dataService
 91    ) {
 92        $this->_requestDescription = $requestDescription;
 93        $this->_dataService = $dataService;
 94        $requestTargetKind = $requestDescription->getTargetKind();
 95        $isSingleResult = $requestDescription->isSingleResult();
 96        $requestCountOption = $requestDescription->getRequestCountOption();
 97        $this->_setQueryApplicable 
 98            = ($requestTargetKind == RequestTargetKind::RESOURCE && !$isSingleResult)
 99                || $requestCountOption == RequestCountOption::VALUE_ONLY;
100        $this->_pagingApplicable 
101            = $this->_requestDescription->getTargetKind() == RequestTargetKind::RESOURCE
102                && !$this->_requestDescription->isSingleResult() 
103                && ($requestCountOption != RequestCountOption::VALUE_ONLY);
104        $targetResourceType = $this->_requestDescription->getTargetResourceType();
105        $targetResourceSetWrapper 
106            = $this->_requestDescription->getTargetResourceSetWrapper();
107        $this->_expandSelectApplicable = !is_null($targetResourceType) 
108            && !is_null($targetResourceSetWrapper)
109            && $targetResourceType->getResourceTypeKind() == ResourceTypeKind::ENTITY
110            && !$this->_requestDescription->isLinkUri();
111        
112    }
113
114    /**
115     * Process the odata query options and update RequestDescription
116     * accordingly. 
117     * 
118     * @param RequestDescription &$requestDescription Description of the request 
119     *                                                submitted by client.
120     * @param DataService        &$dataService        Reference to the data service.
121     * 
122     * @return void
123     * 
124     * @throws ODataException
125     */
126    public static function process(RequestDescription &$requestDescription, 
127        DataService &$dataService
128    ) {
129        $queryProcessor = new QueryProcessor($requestDescription, $dataService);
130        if ($requestDescription->getTargetSource() == RequestTargetSource::NONE) {
131            //A service directory, metadata or batch request
132            $queryProcessor->_checkForEmptyQueryArguments();
133        } else {
134            $queryProcessor->_processQuery();
135        }
136
137        unset($queryProcessor);
138    }
139
140    /**
141     * Processes the odata query options in the request uri and update
142     * the request description instance with processed details.
143     * 
144     * @return void
145     * 
146     * @throws ODataException If any error occured while processing the 
147     *                        query options.
148     */
149    private function _processQuery()
150    {
151        try {
152            $this->_processSkipAndTop();
153            $this->_processOrderBy();
154            $this->_processFilter();
155            $this->_processCount();
156            $this->_processSkipToken();
157            $this->_processExpandAndSelect();
158        } catch (ODataException $odataException) {
159            throw $odataException;
160        }
161    }
162
163    /**
164     * Process $skip and $top options
165     * 
166     * @return void
167     * 
168     * @throws ODataException Throws syntax error if the $skip or $top option
169     *                        is specified with non-integer value, throws
170     *                        bad request error if the $skip or $top option
171     *                        is not applicable for the requested resource. 
172     */
173    private function _processSkipAndTop()
174    {
175        $value = null;
176        if ($this->_readSkipOrTopOption(
177            ODataConstants::HTTPQUERY_STRING_SKIP,
178            $value
179        )
180        ) {
181            $this->_requestDescription->setSkipCount($value);
182        }
183
184        $pageSize = 0;
185        $isPagingRequired = $this->_isSSPagingRequired();
186        if ($isPagingRequired) {
187            $pageSize = $this->_requestDescription
188                ->getTargetResourceSetWrapper()
189                ->getResourceSetPageSize(); 
190        }
191
192        if ($this->_readSkipOrTopOption(
193            ODataConstants::HTTPQUERY_STRING_TOP, 
194            $value
195        )
196        ) {
197            $this->_requestDescription->setTopOptionCount($value);
198            if ($isPagingRequired && $pageSize < $value) {
199                //If $top is greater than or equal to page size, 
200                //we will need a $skiptoken and thus our response 
201                //will be 2.0
202                $this->_requestDescription
203                    ->raiseResponseVersion(2, 0, $this->_dataService);
204                $this->_requestDescription->setTopCount($pageSize);
205            } else {
206                $this->_requestDescription->setTopCount($value);
207            }
208        } else if ($isPagingRequired) {
209            $this->_requestDescription
210                ->raiseResponseVersion(2, 0, $this->_dataService);
211            $this->_requestDescription->setTopCount($pageSize);
212        }
213
214        if (!is_null($this->_requestDescription->getSkipCount()) 
215            || !is_null($this->_requestDescription->getTopCount())
216        ) {
217            $this->_checkSetQueryApplicable();
218        }
219    }
220
221    /**
222     * Process $orderby option, This function requires _processSkipAndTopOption
223     * function to be already called as this function need to know whether 
224     * client has requested for skip, top or paging is enabled for the 
225     * requested resource in these cases function generates additional orderby
226     * expression using keys.
227     * 
228     * @return void
229     * 
230     * @throws ODataException If any error occurs while parsing orderby option.
231     */
232    private function _processOrderBy()
233    {
234        $orderBy = $this->_dataService->getHost()->getQueryStringItem(
235            ODataConstants::HTTPQUERY_STRING_ORDERBY
236        );
237
238        if (!is_null($orderBy)) {
239            $this->_checkSetQueryApplicable();
240        }
241
242        $targetResourceType = $this->_requestDescription->getTargetResourceType();
243        //assert($targetResourceType != null)
244        /**
245         * We need to do sorting in the folowing cases, irrespective of 
246         * $orderby clause is present or not.
247         * 1. If $top or $skip is specified
248         *     skip and take will be applied on sorted list only. If $skip 
249         *     is specified then RequestDescription::getSkipCount will give 
250         *     non-null value. If $top is specified then 
251         *     RequestDescription::getTopCount will give non-null value.
252         * 2. If server side paging is enabled for the requested resource
253         *     If server-side paging is enabled for the requested resource then 
254         *     RequestDescription::getTopCount will give non-null value.
255         *      
256         */
257        if (!is_null($this->_requestDescription->getSkipCount())
258            || !is_null($this->_requestDescription->getTopCount())
259        ) {
260            $orderBy = !is_null($orderBy) ? $orderBy . ', ' : null;
261            $keys = array_keys($targetResourceType->getKeyProperties());
262            //assert(!empty($keys))
263            foreach ($keys as $key) {
264                $orderBy = $orderBy . $key . ', ';
265            }
266
267            $orderBy = rtrim($orderBy, ', ');
268        }
269
270        if (!is_null($orderBy)) {
271            try {
272                $internalOrderByInfo = OrderByParser::parseOrderByClause(
273                    $this->_requestDescription->getTargetResourceSetWrapper(), 
274                    $targetResourceType, 
275                    $orderBy, 
276                    $this->_dataService->getMetadataQueryProviderWrapper()
277                );
278
279                $this->_requestDescription->setInternalOrderByInfo(
280                    $internalOrderByInfo
281                );
282            } catch (ODataException $odataException) {
283                throw $odataException;
284            }
285        }
286    }
287
288    /**
289     * Process the $filter option in the request and update request decription.
290     * 
291     * @return void
292     * 
293     * @throws ODataException Throws error in the following cases:
294     *                          (1) If $filter cannot be applied to the 
295     *                              resource targetted by the request uri
296     *                          (2) If any error occured while parsing and
297     *                              translating the odata $filter expression
298     *                              to expression tree
299     *                          (3) If any error occured while generating
300     *                              php expression from expression tree
301     */ 
302    private function _processFilter()
303    {
304        $filter = $this->_dataService->getHost()->getQueryStringItem(
305            ODataConstants::HTTPQUERY_STRING_FILTER
306        );
307        if (!is_null($filter)) {
308            $requestTargetKind = $this->_requestDescription->getTargetKind();
309            if (!($requestTargetKind == RequestTargetKind::RESOURCE 
310                || $requestTargetKind == RequestTargetKind::COMPLEX_OBJECT 
311                || $this->_requestDescription->getRequestCountOption() == RequestCountOption::VALUE_ONLY)
312            ) {
313                ODataException::createBadRequestError(
314                    Messages::queryProcessorQueryFilterOptionNotApplicable()
315                );
316            }
317            $resourceType = $this->_requestDescription->getTargetResourceType();
318            try {
319            	$expressionProvider = $this->_dataService->getMetadataQueryProviderWrapper()->getExpressionProvider();      
320                $internalFilterInfo = ExpressionParser2::parseExpression2(
321                    $filter, $resourceType, $expressionProvider
322                );
323                $this->_requestDescription->setInternalFilterInfo(
324                    $internalFilterInfo
325                );                
326            } catch (ODataException $odataException) {
327                throw $odataException;
328            }
329        }
330    }
331
332    /**
333     * Process the $inlinecount option and update the request description.
334     *
335     * @return void
336     * 
337     * @throws ODataException Throws bad request error in the following cases
338     *                          (1) If $inlinecount is disabled by the developer
339     *                          (2) If both $count and $inlinecount specified
340     *                          (3) If $inlinecount value is unknown
341     *                          (4) If capability negotiation over version fails
342     */
343    private function _processCount()
344    {
345        $inlineCount = $this->_dataService->getHost()->getQueryStringItem(
346            ODataConstants::HTTPQUERY_STRING_INLINECOUNT
347        );
348
349        if (!is_null($inlineCount)) {
350            if (!$this->_dataService->getServiceConfiguration()->getAcceptCountRequests()) {
351                ODataException::createBadRequestError(
352                    Messages::dataServiceConfigurationCountNotAccepted()
353                );
354            }
355
356            $inlineCount = trim($inlineCount);
357            if ($inlineCount === ODataConstants::URI_ROWCOUNT_OFFOPTION) {
358                return;
359            }
360
361            if ($this->_requestDescription->getRequestCountOption() == RequestCountOption::VALUE_ONLY
362            ) {
363                ODataException::createBadRequestError(
364                    Messages::queryProcessorInlineCountWithValueCount()
365                );
366            }
367
368            $this->_checkSetQueryApplicable();
369            if ($inlineCount === ODataConstants::URI_ROWCOUNT_ALLOPTION) {
370                $this->_requestDescription->setRequestCountOption(
371                    RequestCountOption::INLINE
372                );
373                $this->_requestDescription->raiseMinimumVersionRequirement(
374                    2, 
375                    0, 
376                    $this->_dataService
377                );
378                $this->_requestDescription->raiseResponseVersion(
379                    2, 
380                    0, 
381                    $this->_dataService
382                );
383            } else {
384                ODataException::createBadRequestError(
385                    Messages::queryProcessorInvalidInlineCountOptionError()
386                );
387            }
388        }
389    }
390
391    /**
392     * Process the $skiptoken option in the request and update the request 
393     * description, this function requires _processOrderBy method to be
394     * already invoked.
395     * 
396     * @return void
397     * 
398     * @throws ODataException Throws bad request error in the following cases
399     *                          (1) If $skiptoken cannot be applied to the 
400     *                              resource targetted by the request uri
401     *                          (2) If paging is not enabled for the resource
402     *                              targetted by the request uri
403     *                          (3) If parsing of $skiptoken fails
404     *                          (4) If capability negotiation over version fails
405     */
406    private function _processSkipToken()
407    {
408        $skipToken = $this->_dataService->getHost()->getQueryStringItem(
409            ODataConstants::HTTPQUERY_STRING_SKIPTOKEN
410        );
411        if (!is_null($skipToken)) {
412            if (!$this->_pagingApplicable) {
413                ODataException::createBadRequestError(
414                    Messages::queryProcessorSkipTokenNotAllowed()
415                );
416            }
417
418            if (!$this->_isSSPagingRequired()) {
419                ODataException::createBadRequestError(
420                    Messages::queryProcessorSkipTokenCannotBeAppliedForNonPagedResourceSet()
421                );
422            }
423
424            $internalOrderByInfo 
425                = $this->_requestDescription->getInternalOrderByInfo();
426            //assert($internalOrderByInfo != null)
427            $targetResourceType 
428                = $this->_requestDescription->getTargetResourceType();
429            //assert($targetResourceType != null)
430            try {
431                $internalSkipTokenInfo = SkipTokenParser::parseSkipTokenClause(
432                    $targetResourceType, 
433                    $internalOrderByInfo, 
434                    $skipToken
435                );
436                $this->_requestDescription
437                    ->setInternalSkipTokenInfo($internalSkipTokenInfo);
438                $this->_requestDescription->raiseMinimumVersionRequirement(
439                    2, 
440                    0, 
441                    $this->_dataService
442                );
443                $this->_requestDescription->raiseResponseVersion(
444                    2, 
445                    0, 
446                    $this->_dataService
447                );
448            } catch (ODataException $odataException) {
449                throw $odataException;
450            }
451        }
452    }
453
454    /**
455     * Process the $expand and $select option and update the request description.
456     * 
457     * @return void
458     * 
459     * @throws ODataException Throws bad request error in the following cases
460     *                          (1) If $expand or select cannot be applied to the
461     *                              requested resource.
462     *                          (2) If projection is disabled by the developer
463     *                          (3) If some error occurs while parsing the options
464     */
465    private function _processExpandAndSelect()
466    {
467        $expand = $this->_dataService->getHost()->getQueryStringItem(
468            ODataConstants::HTTPQUERY_STRING_EXPAND
469        );
470
471        if (!is_null($expand)) {
472            $this->_checkExpandOrSelectApplicable(
473                ODataConstants::HTTPQUERY_STRING_EXPAND
474            );
475        }
476
477        $select = $this->_dataService->getHost()->getQueryStringItem(
478            ODataConstants::HTTPQUERY_STRING_SELECT
479        );
480
481        if (!is_null($select)) {
482            if (!$this->_dataService->getServiceConfiguration()->getAcceptProjectionRequests()) {
483                ODataException::createBadRequestError(
484                    Messages::dataServiceConfigurationProjectionsNotAccepted()
485                );
486            }
487
488            $this->_checkExpandOrSelectApplicable(
489                ODataConstants::HTTPQUERY_STRING_SELECT
490            );
491        }
492
493        // We will generate RootProjectionNode in case of $link request also, but
494        // expand and select in this case must be null (we are ensuring this above)
495        // 'RootProjectionNode' is required while generating next page Link
496        if ($this->_expandSelectApplicable 
497            || $this->_requestDescription->isLinkUri()
498        ) {
499            try {
500                 $rootProjectionNode = ExpandProjectionParser::parseExpandAndSelectClause(
501                     $this->_requestDescription->getTargetResourceSetWrapper(), 
502                     $this->_requestDescription->getTargetResourceType(), 
503                     $this->_requestDescription->getInternalOrderByInfo(), 
504                     $this->_requestDescription->getSkipCount(), 
505                     $this->_requestDescription->getTopCount(), 
506                     $expand, 
507                     $select, 
508                     $this->_dataService->getMetadataQueryProviderWrapper()
509                 );
510                if ($rootProjectionNode->isSelectionSpecified()) {
511                    $this->_requestDescription->raiseMinimumVersionRequirement(
512                        2, 
513                        0, 
514                        $this->_dataService
515                    );
516                }
517
518                if ($rootProjectionNode->hasPagedExpandedResult()) {
519                    $this->_requestDescription->raiseResponseVersion(
520                        2, 
521                        0, 
522                        $this->_dataService
523                    );
524                }
525                $this->_requestDescription->setRootProjectionNode(
526                    $rootProjectionNode
527                );
528            } catch (ODataException $odataException) {
529                    throw $odataException;
530            }
531        }
532    } 
533
534    /**
535     * Is server side paging is configured, this function return true
536     * if the resource targetted by the resource path is applicable
537     * for paging and paging is enabled for the targetted resource set
538     * else false.
539     * 
540     * @return boolean
541     */
542    private function _isSSPagingRequired()
543    {
544        if ($this->_pagingApplicable) {
545            $targetResourceSetWrapper 
546                = $this->_requestDescription->getTargetResourceSetWrapper();
547            //assert($targetResourceSetWrapper != NULL)
548            return ($targetResourceSetWrapper->getResourceSetPageSize() != 0);
549        }
550
551        return false;
552    }
553
554    /**
555     * Read skip or top query option value which is expected to be positive 
556     * integer. 
557     * 
558     * @param string $queryItem The name of the query item to read from request
559     *                          uri ($skip or $top).
560     * @param int    &$value    On return, If the requested query item is 
561     *                          present with a valid integer value then this
562     *                          argument will holds that integer value 
563     *                          otherwise holds zero.
564     * 
565     * @return boolean True     If the requested query item with valid integer 
566     *                          value is present in the request, false query 
567     *                          item is absent in the request uri. 
568     * 
569     * @throws ODataException   Throws syntax error if the requested argument 
570     *                          is present and it is not an integer.
571     */
572    private function _readSkipOrTopOption($queryItem, &$value)
573    {
574        $value = $this->_dataService->getHost()->getQueryStringItem($queryItem);
575        if (!is_null($value)) {
576            $int = new Int32();
577            if (!$int->validate($value, $outValue)) {
578                ODataException::createSyntaxError(
579                    Messages::queryProcessorIncorrectArgumentFormat(
580                        $queryItem, 
581                        $value
582                    )
583                );
584            }
585
586            $value = intval($value);
587            if ($value < 0) {
588                ODataException::createSyntaxError(
589                    Messages::queryProcessorIncorrectArgumentFormat(
590                        $queryItem, 
591                        $value
592                    )
593                );
594            }
595
596            return true;
597        }
598
599        $value = 0;
600        return false;
601    }
602 
603    /**
604     * Checks whether client request contains any odata query options.
605     * 
606     * @return void
607     * 
608     * @throws ODataException Throws bad request error if client request 
609     *                        includes any odata query option.
610     */
611    private function _checkForEmptyQueryArguments()
612    {
613        $dataServiceHost = $this->_dataService->getHost();
614        if (!is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_FILTER)) 
615            || !is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_EXPAND)) 
616            || !is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_INLINECOUNT)) 
617            || !is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_ORDERBY)) 
618            || !is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_SELECT)) 
619            || !is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_SKIP)) 
620            || !is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_SKIPTOKEN)) 
621            || !is_null($dataServiceHost->getQueryStringItem(ODataConstants::HTTPQUERY_STRING_TOP))
622        ) {
623            ODataException::createBadRequestError(
624                Messages::queryProcessorNoQueryOptionsApplicable()
625            );
626        }
627    }
628
629    /**
630     * To check whether the the query options $orderby, $inlinecount, $skip
631     * or $top is applicable for the current requested resource.
632     * 
633     * @return void
634     * 
635     * @throws ODataException Throws bad request error if any of the query 
636     *                        options $orderby, $inlinecount, $skip or $top
637     *                        cannot be applied to the requested resource.
638     *
639     */
640    private function _checkSetQueryApplicable()
641    {
642        if (!$this->_setQueryApplicable) { 
643            ODataException::createBadRequestError(
644                Messages::queryProcessorQuerySetOptionsNotApplicable()
645            );
646        }
647    }
648
649    /**
650     * To check whether the the query options $select, $expand
651     * is applicable for the current requested resource.
652     * 
653     * @param string $queryItem The query option to check.
654     * 
655     * @return void
656     * 
657     * @throws ODataException Throws bad request error if the query 
658     *                        options $select, $expand cannot be 
659     *                        applied to the requested resource. 
660     */
661    private function _checkExpandOrSelectApplicable($queryItem)
662    {
663        if (!$this->_expandSelectApplicable) {
664            ODataException::createBadRequestError(
665                Messages::queryProcessorSelectOrExpandOptionNotApplicable($queryItem)
666            );
667        }
668    }
669}
670?>