PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Sparql/Client.php

http://github.com/njh/easyrdf
PHP | 417 lines | 275 code | 23 blank | 119 comment | 18 complexity | 6d7912e572224c271013b1e21926d005 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. <?php
  2. namespace EasyRdf\Sparql;
  3. /**
  4. * EasyRdf
  5. *
  6. * LICENSE
  7. *
  8. * Copyright (c) Nicholas J Humfrey. All rights reserved.
  9. *
  10. * Redistribution and use in source and binary forms, with or without
  11. * modification, are permitted provided that the following conditions are met:
  12. * 1. Redistributions of source code must retain the above copyright
  13. * notice, this list of conditions and the following disclaimer.
  14. * 2. Redistributions in binary form must reproduce the above copyright notice,
  15. * this list of conditions and the following disclaimer in the documentation
  16. * and/or other materials provided with the distribution.
  17. * 3. The name of the author 'Nicholas J Humfrey" may be used to endorse or
  18. * promote products derived from this software without specific prior
  19. * written permission.
  20. *
  21. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  22. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  23. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  24. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  25. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  26. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  27. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  28. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  29. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  30. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. * POSSIBILITY OF SUCH DAMAGE.
  32. *
  33. * @package EasyRdf
  34. * @copyright Copyright (c) Nicholas J Humfrey
  35. * @license https://www.opensource.org/licenses/bsd-license.php
  36. */
  37. use EasyRdf\Exception;
  38. use EasyRdf\Format;
  39. use EasyRdf\Graph;
  40. use EasyRdf\Http;
  41. use EasyRdf\RdfNamespace;
  42. use EasyRdf\Utils;
  43. /**
  44. * Class for making SPARQL queries using the SPARQL 1.1 Protocol
  45. *
  46. * @package EasyRdf
  47. * @copyright Copyright (c) Nicholas J Humfrey
  48. * @license https://www.opensource.org/licenses/bsd-license.php
  49. */
  50. class Client
  51. {
  52. /** The query/read address of the SPARQL Endpoint */
  53. private $queryUri = null;
  54. private $queryUri_has_params = false;
  55. /** The update/write address of the SPARQL Endpoint */
  56. private $updateUri = null;
  57. /** Create a new SPARQL endpoint client
  58. *
  59. * If the query and update endpoints are the same, then you
  60. * only need to give a single URI.
  61. *
  62. * @param string $queryUri The address of the SPARQL Query Endpoint
  63. * @param string $updateUri Optional address of the SPARQL Update Endpoint
  64. */
  65. public function __construct($queryUri, $updateUri = null)
  66. {
  67. $this->queryUri = $queryUri;
  68. if (strlen(parse_url($queryUri, PHP_URL_QUERY)) > 0) {
  69. $this->queryUri_has_params = true;
  70. } else {
  71. $this->queryUri_has_params = false;
  72. }
  73. if ($updateUri) {
  74. $this->updateUri = $updateUri;
  75. } else {
  76. $this->updateUri = $queryUri;
  77. }
  78. }
  79. /** Get the URI of the SPARQL query endpoint
  80. *
  81. * @return string The query URI of the SPARQL endpoint
  82. */
  83. public function getQueryUri()
  84. {
  85. return $this->queryUri;
  86. }
  87. /** Get the URI of the SPARQL update endpoint
  88. *
  89. * @return string The query URI of the SPARQL endpoint
  90. */
  91. public function getUpdateUri()
  92. {
  93. return $this->updateUri;
  94. }
  95. /**
  96. * @depredated
  97. * @ignore
  98. */
  99. public function getUri()
  100. {
  101. return $this->queryUri;
  102. }
  103. /** Make a query to the SPARQL endpoint
  104. *
  105. * SELECT and ASK queries will return an object of type
  106. * EasyRdf\Sparql\Result.
  107. *
  108. * CONSTRUCT and DESCRIBE queries will return an object
  109. * of type EasyRdf\Graph.
  110. *
  111. * @param string $query The query string to be executed
  112. *
  113. * @return Result|\EasyRdf\Graph Result of the query.
  114. */
  115. public function query($query)
  116. {
  117. return $this->request('query', $query);
  118. }
  119. /** Count the number of triples in a SPARQL 1.1 endpoint
  120. *
  121. * Performs a SELECT query to estriblish the total number of triples.
  122. *
  123. * Counts total number of triples by default but a conditional triple pattern
  124. * can be given to count of a subset of all triples.
  125. *
  126. * @param string $condition Triple-pattern condition for the count query
  127. *
  128. * @return integer The number of triples
  129. */
  130. public function countTriples($condition = '?s ?p ?o')
  131. {
  132. // SELECT (COUNT(*) AS ?count)
  133. // WHERE {
  134. // {?s ?p ?o}
  135. // UNION
  136. // {GRAPH ?g {?s ?p ?o}}
  137. // }
  138. $result = $this->query('SELECT (COUNT(*) AS ?count) {'.$condition.'}');
  139. return $result[0]->count->getValue();
  140. }
  141. /** Get a list of named graphs from a SPARQL 1.1 endpoint
  142. *
  143. * Performs a SELECT query to get a list of the named graphs
  144. *
  145. * @param string $limit Optional limit to the number of results
  146. *
  147. * @return \EasyRdf\Resource[] array of objects for each named graph
  148. */
  149. public function listNamedGraphs($limit = null)
  150. {
  151. $query = "SELECT DISTINCT ?g WHERE {GRAPH ?g {?s ?p ?o}}";
  152. if (!is_null($limit)) {
  153. $query .= " LIMIT ".(int)$limit;
  154. }
  155. $result = $this->query($query);
  156. // Convert the result object into an array of resources
  157. $graphs = array();
  158. foreach ($result as $row) {
  159. array_push($graphs, $row->g);
  160. }
  161. return $graphs;
  162. }
  163. /** Make an update request to the SPARQL endpoint
  164. *
  165. * Successful responses will return the HTTP response object
  166. *
  167. * Unsuccessful responses will throw an exception
  168. *
  169. * @param string $query The update query string to be executed
  170. *
  171. * @return \EasyRdf\Http\Response HTTP response
  172. */
  173. public function update($query)
  174. {
  175. return $this->request('update', $query);
  176. }
  177. public function insert($data, $graphUri = null)
  178. {
  179. #$this->updateData('INSET',
  180. $query = 'INSERT DATA {';
  181. if ($graphUri) {
  182. $query .= "GRAPH <$graphUri> {";
  183. }
  184. $query .= $this->convertToTriples($data);
  185. if ($graphUri) {
  186. $query .= "}";
  187. }
  188. $query .= '}';
  189. return $this->update($query);
  190. }
  191. protected function updateData($operation, $data, $graphUri = null)
  192. {
  193. $query = "$operation DATA {";
  194. if ($graphUri) {
  195. $query .= "GRAPH <$graphUri> {";
  196. }
  197. $query .= $this->convertToTriples($data);
  198. if ($graphUri) {
  199. $query .= "}";
  200. }
  201. $query .= '}';
  202. return $this->update($query);
  203. }
  204. public function clear($graphUri, $silent = false)
  205. {
  206. $query = "CLEAR";
  207. if ($silent) {
  208. $query .= " SILENT";
  209. }
  210. if (preg_match('/^all|named|default$/i', $graphUri)) {
  211. $query .= " $graphUri";
  212. } else {
  213. $query .= " GRAPH <$graphUri>";
  214. }
  215. return $this->update($query);
  216. }
  217. /*
  218. * Internal function to make an HTTP request to SPARQL endpoint
  219. *
  220. * @ignore
  221. */
  222. protected function request($type, $query)
  223. {
  224. $processed_query = $this->preprocessQuery($query);
  225. $response = $this->executeQuery($processed_query, $type);
  226. if (!$response->isSuccessful()) {
  227. throw new Http\Exception("HTTP request for SPARQL query failed", 0, null, $response->getBody());
  228. }
  229. if ($response->getStatus() == 204) {
  230. // No content
  231. return $response;
  232. }
  233. return $this->parseResponseToQuery($response);
  234. }
  235. protected function convertToTriples($data)
  236. {
  237. if (is_string($data)) {
  238. return $data;
  239. } elseif (is_object($data) and $data instanceof Graph) {
  240. # FIXME: insert Turtle when there is a way of seperateing out the prefixes
  241. return $data->serialise('ntriples');
  242. } else {
  243. throw new Exception(
  244. "Don't know how to convert to triples for SPARQL query"
  245. );
  246. }
  247. }
  248. /**
  249. * Adds missing prefix-definitions to the query
  250. *
  251. * Overriding classes may execute arbitrary query-alteration here
  252. *
  253. * @param string $query
  254. * @return string
  255. */
  256. protected function preprocessQuery($query)
  257. {
  258. // Check for undefined prefixes
  259. $prefixes = '';
  260. foreach (RdfNamespace::namespaces() as $prefix => $uri) {
  261. if (strpos($query, "{$prefix}:") !== false and
  262. strpos($query, "PREFIX {$prefix}:") === false
  263. ) {
  264. $prefixes .= "PREFIX {$prefix}: <{$uri}>\n";
  265. }
  266. }
  267. return $prefixes . $query;
  268. }
  269. /**
  270. * Build http-client object, execute request and return a response
  271. *
  272. * @param string $processed_query
  273. * @param string $type Should be either "query" or "update"
  274. *
  275. * @return Http\Response|\Zend\Http\Response
  276. * @throws Exception
  277. */
  278. protected function executeQuery($processed_query, $type)
  279. {
  280. $client = Http::getDefaultHttpClient();
  281. $client->resetParameters();
  282. // Tell the server which response formats we can parse
  283. $sparql_results_types = array(
  284. 'application/sparql-results+json' => 1.0,
  285. 'application/sparql-results+xml' => 0.8
  286. );
  287. if ($type == 'update') {
  288. // accept anything, as "response body of a […] update request is implementation defined"
  289. // @see http://www.w3.org/TR/sparql11-protocol/#update-success
  290. $accept = Format::getHttpAcceptHeader($sparql_results_types);
  291. $this->setHeaders($client, 'Accept', $accept);
  292. $client->setMethod('POST');
  293. $client->setUri($this->updateUri);
  294. $client->setRawData($processed_query);
  295. $this->setHeaders($client, 'Content-Type', 'application/sparql-update');
  296. } elseif ($type == 'query') {
  297. $re = '(?:(?:\s*BASE\s*<.*?>\s*)|(?:\s*PREFIX\s+.+:\s*<.*?>\s*))*'.
  298. '(CONSTRUCT|SELECT|ASK|DESCRIBE)[\W]';
  299. $result = null;
  300. $matched = mb_eregi($re, $processed_query, $result);
  301. if (false === $matched or count($result) !== 2) {
  302. // non-standard query. is this something non-standard?
  303. $query_verb = null;
  304. } else {
  305. $query_verb = strtoupper($result[1]);
  306. }
  307. if ($query_verb === 'SELECT' or $query_verb === 'ASK') {
  308. // only "results"
  309. $accept = Format::formatAcceptHeader($sparql_results_types);
  310. } elseif ($query_verb === 'CONSTRUCT' or $query_verb === 'DESCRIBE') {
  311. // only "graph"
  312. $accept = Format::getHttpAcceptHeader();
  313. } else {
  314. // both
  315. $accept = Format::getHttpAcceptHeader($sparql_results_types);
  316. }
  317. $this->setHeaders($client, 'Accept', $accept);
  318. $encodedQuery = 'query=' . urlencode($processed_query);
  319. // Use GET if the query is less than 2kB
  320. // 2046 = 2kB minus 1 for '?' and 1 for NULL-terminated string on server
  321. if (strlen($encodedQuery) + strlen($this->queryUri) <= 2046) {
  322. $delimiter = $this->queryUri_has_params ? '&' : '?';
  323. $client->setMethod('GET');
  324. $client->setUri($this->queryUri . $delimiter . $encodedQuery);
  325. } else {
  326. // Fall back to POST instead (which is un-cacheable)
  327. $client->setMethod('POST');
  328. $client->setUri($this->queryUri);
  329. $client->setRawData($encodedQuery);
  330. $this->setHeaders($client, 'Content-Type', 'application/x-www-form-urlencoded');
  331. }
  332. } else {
  333. throw new Exception('unexpected request-type: '.$type);
  334. }
  335. if ($client instanceof \Zend\Http\Client) {
  336. return $client->send();
  337. } else {
  338. return $client->request();
  339. }
  340. }
  341. /**
  342. * Parse HTTP-response object into a meaningful result-object.
  343. *
  344. * Can be overridden to do custom processing
  345. *
  346. * @param Http\Response|\Zend\Http\Response $response
  347. * @return Graph|Result
  348. */
  349. protected function parseResponseToQuery($response)
  350. {
  351. list($content_type,) = Utils::parseMimeType($response->getHeader('Content-Type'));
  352. if (strpos($content_type, 'application/sparql-results') === 0) {
  353. $result = new Result($response->getBody(), $content_type);
  354. return $result;
  355. } else {
  356. $result = new Graph($this->queryUri, $response->getBody(), $content_type);
  357. return $result;
  358. }
  359. }
  360. /**
  361. * Proxy function to allow usage of our Client as well as Zend\Http v2.
  362. *
  363. * Zend\Http\Client only accepts an array as first parameter, but our Client wants a name-value pair.
  364. *
  365. * @see https://framework.zend.com/apidoc/2.4/classes/Zend.Http.Client.html#method_setHeaders
  366. *
  367. * @todo Its only a temporary fix, should be replaced or refined in the future.
  368. */
  369. protected function setHeaders($client, $name, $value)
  370. {
  371. if ($client instanceof \Zend\Http\Client) {
  372. $client->setHeaders([$name => $value]);
  373. } else {
  374. $client->setHeaders($name, $value);
  375. }
  376. }
  377. }