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