PageRenderTime 41ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/jsonld.php

http://github.com/digitalbazaar/php-json-ld
PHP | 6038 lines | 3804 code | 546 blank | 1688 comment | 887 complexity | d9edef1298ff26620ddd5bd83d40cc96 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * PHP implementation of the JSON-LD API.
  4. * Version: 0.4.8-dev
  5. *
  6. * @author Dave Longley
  7. *
  8. * BSD 3-Clause License
  9. * Copyright (c) 2011-2014 Digital Bazaar, Inc.
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions are met:
  14. *
  15. * Redistributions of source code must retain the above copyright notice,
  16. * this list of conditions and the following disclaimer.
  17. *
  18. * Redistributions in binary form must reproduce the above copyright
  19. * notice, this list of conditions and the following disclaimer in the
  20. * documentation and/or other materials provided with the distribution.
  21. *
  22. * Neither the name of the Digital Bazaar, Inc. nor the names of its
  23. * contributors may be used to endorse or promote products derived from
  24. * this software without specific prior written permission.
  25. *
  26. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  27. * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  28. * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
  29. * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  30. * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  31. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
  32. * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  33. * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  34. * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  35. * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  36. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  37. */
  38. /**
  39. * Performs JSON-LD compaction.
  40. *
  41. * @param mixed $input the JSON-LD object to compact.
  42. * @param mixed $ctx the context to compact with.
  43. * @param assoc [$options] options to use:
  44. * [base] the base IRI to use.
  45. * [graph] true to always output a top-level graph (default: false).
  46. * [documentLoader(url)] the document loader.
  47. *
  48. * @return mixed the compacted JSON-LD output.
  49. */
  50. function jsonld_compact($input, $ctx, $options=array()) {
  51. $p = new JsonLdProcessor();
  52. return $p->compact($input, $ctx, $options);
  53. }
  54. /**
  55. * Performs JSON-LD expansion.
  56. *
  57. * @param mixed $input the JSON-LD object to expand.
  58. * @param assoc[$options] the options to use:
  59. * [base] the base IRI to use.
  60. * [documentLoader(url)] the document loader.
  61. *
  62. * @return array the expanded JSON-LD output.
  63. */
  64. function jsonld_expand($input, $options=array()) {
  65. $p = new JsonLdProcessor();
  66. return $p->expand($input, $options);
  67. }
  68. /**
  69. * Performs JSON-LD flattening.
  70. *
  71. * @param mixed $input the JSON-LD to flatten.
  72. * @param mixed $ctx the context to use to compact the flattened output, or
  73. * null.
  74. * @param [options] the options to use:
  75. * [base] the base IRI to use.
  76. * [documentLoader(url)] the document loader.
  77. *
  78. * @return mixed the flattened JSON-LD output.
  79. */
  80. function jsonld_flatten($input, $ctx, $options=array()) {
  81. $p = new JsonLdProcessor();
  82. return $p->flatten($input, $ctx, $options);
  83. }
  84. /**
  85. * Performs JSON-LD framing.
  86. *
  87. * @param mixed $input the JSON-LD object to frame.
  88. * @param stdClass $frame the JSON-LD frame to use.
  89. * @param assoc [$options] the framing options.
  90. * [base] the base IRI to use.
  91. * [embed] default @embed flag (default: true).
  92. * [explicit] default @explicit flag (default: false).
  93. * [requireAll] default @requireAll flag (default: true).
  94. * [omitDefault] default @omitDefault flag (default: false).
  95. * [documentLoader(url)] the document loader.
  96. *
  97. * @return stdClass the framed JSON-LD output.
  98. */
  99. function jsonld_frame($input, $frame, $options=array()) {
  100. $p = new JsonLdProcessor();
  101. return $p->frame($input, $frame, $options);
  102. }
  103. /**
  104. * **Experimental**
  105. *
  106. * Links a JSON-LD document's nodes in memory.
  107. *
  108. * @param mixed $input the JSON-LD document to link.
  109. * @param mixed $ctx the JSON-LD context to apply or null.
  110. * @param assoc [$options] the options to use:
  111. * [base] the base IRI to use.
  112. * [expandContext] a context to expand with.
  113. * [documentLoader(url)] the document loader.
  114. *
  115. * @return the linked JSON-LD output.
  116. */
  117. function jsonld_link($input, $ctx, $options) {
  118. // API matches running frame with a wildcard frame and embed: '@link'
  119. // get arguments
  120. $frame = new stdClass();
  121. if($ctx) {
  122. $frame->{'@context'} = $ctx;
  123. }
  124. $frame->{'@embed'} = '@link';
  125. return jsonld_frame($input, $frame, $options);
  126. };
  127. /**
  128. * Performs RDF dataset normalization on the given input. The input is
  129. * JSON-LD unless the 'inputFormat' option is used. The output is an RDF
  130. * dataset unless the 'format' option is used.
  131. *
  132. * @param mixed $input the JSON-LD object to normalize.
  133. * @param assoc [$options] the options to use:
  134. * [base] the base IRI to use.
  135. * [intputFormat] the format if input is not JSON-LD:
  136. * 'application/nquads' for N-Quads.
  137. * [format] the format if output is a string:
  138. * 'application/nquads' for N-Quads.
  139. * [documentLoader(url)] the document loader.
  140. *
  141. * @return mixed the normalized output.
  142. */
  143. function jsonld_normalize($input, $options=array()) {
  144. $p = new JsonLdProcessor();
  145. return $p->normalize($input, $options);
  146. }
  147. /**
  148. * Converts an RDF dataset to JSON-LD.
  149. *
  150. * @param mixed $input a serialized string of RDF in a format specified
  151. * by the format option or an RDF dataset to convert.
  152. * @param assoc [$options] the options to use:
  153. * [format] the format if input not an array:
  154. * 'application/nquads' for N-Quads (default).
  155. * [useRdfType] true to use rdf:type, false to use @type
  156. * (default: false).
  157. * [useNativeTypes] true to convert XSD types into native types
  158. * (boolean, integer, double), false not to (default: false).
  159. *
  160. * @return array the JSON-LD output.
  161. */
  162. function jsonld_from_rdf($input, $options=array()) {
  163. $p = new JsonLdProcessor();
  164. return $p->fromRDF($input, $options);
  165. }
  166. /**
  167. * Outputs the RDF dataset found in the given JSON-LD object.
  168. *
  169. * @param mixed $input the JSON-LD object.
  170. * @param assoc [$options] the options to use:
  171. * [base] the base IRI to use.
  172. * [format] the format to use to output a string:
  173. * 'application/nquads' for N-Quads.
  174. * [produceGeneralizedRdf] true to output generalized RDF, false
  175. * to produce only standard RDF (default: false).
  176. * [documentLoader(url)] the document loader.
  177. *
  178. * @return mixed the resulting RDF dataset (or a serialization of it).
  179. */
  180. function jsonld_to_rdf($input, $options=array()) {
  181. $p = new JsonLdProcessor();
  182. return $p->toRDF($input, $options);
  183. }
  184. /**
  185. * JSON-encodes (with unescaped slashes) the given stdClass or array.
  186. *
  187. * @param mixed $input the native PHP stdClass or array which will be
  188. * converted to JSON by json_encode().
  189. * @param int $options the options to use.
  190. * [JSON_PRETTY_PRINT] pretty print.
  191. * @param int $depth the maximum depth to use.
  192. *
  193. * @return the encoded JSON data.
  194. */
  195. function jsonld_encode($input, $options=0, $depth=512) {
  196. // newer PHP has a flag to avoid escaped '/'
  197. if(defined('JSON_UNESCAPED_SLASHES')) {
  198. return json_encode($input, JSON_UNESCAPED_SLASHES | $options, $depth);
  199. }
  200. // use a simple string replacement of '\/' to '/'.
  201. return str_replace('\\/', '/', json_encode($input, $options, $depth));
  202. }
  203. /**
  204. * Decodes a serialized JSON-LD object.
  205. *
  206. * @param string $input the JSON-LD input.
  207. *
  208. * @return mixed the resolved JSON-LD object, null on error.
  209. */
  210. function jsonld_decode($input) {
  211. return json_decode($input);
  212. }
  213. /**
  214. * Parses a link header. The results will be key'd by the value of "rel".
  215. *
  216. * Link: <http://json-ld.org/contexts/person.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
  217. *
  218. * Parses as: {
  219. * 'http://www.w3.org/ns/json-ld#context': {
  220. * target: http://json-ld.org/contexts/person.jsonld,
  221. * type: 'application/ld+json'
  222. * }
  223. * }
  224. *
  225. * If there is more than one "rel" with the same IRI, then entries in the
  226. * resulting map for that "rel" will be arrays of objects, otherwise they will
  227. * be single objects.
  228. *
  229. * @param string $header the link header to parse.
  230. *
  231. * @return assoc the parsed result.
  232. */
  233. function jsonld_parse_link_header($header) {
  234. $rval = array();
  235. // split on unbracketed/unquoted commas
  236. if(!preg_match_all(
  237. '/(?:<[^>]*?>|"[^"]*?"|[^,])+/', $header, $entries, PREG_SET_ORDER)) {
  238. return $rval;
  239. }
  240. $r_link_header = '/\s*<([^>]*?)>\s*(?:;\s*(.*))?/';
  241. foreach($entries as $entry) {
  242. if(!preg_match($r_link_header, $entry[0], $match)) {
  243. continue;
  244. }
  245. $result = (object)array('target' => $match[1]);
  246. $params = $match[2];
  247. $r_params = '/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/';
  248. preg_match_all($r_params, $params, $matches, PREG_SET_ORDER);
  249. foreach($matches as $match) {
  250. $result->{$match[1]} = $match[2] ?: $match[3];
  251. }
  252. $rel = property_exists($result, 'rel') ? $result->rel : '';
  253. if(!isset($rval[$rel])) {
  254. $rval[$rel] = $result;
  255. } else if(is_array($rval[$rel])) {
  256. $rval[$rel][] = $result;
  257. } else {
  258. $rval[$rel] = array($rval[$rel], $result);
  259. }
  260. }
  261. return $rval;
  262. }
  263. /**
  264. * Relabels all blank nodes in the given JSON-LD input.
  265. *
  266. * @param mixed input the JSON-LD input.
  267. */
  268. function jsonld_relabel_blank_nodes($input) {
  269. $p = new JsonLdProcessor();
  270. return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input);
  271. }
  272. /** JSON-LD shared in-memory cache. */
  273. global $jsonld_cache;
  274. $jsonld_cache = new stdClass();
  275. /** The default active context cache. */
  276. $jsonld_cache->activeCtx = new ActiveContextCache();
  277. /** Stores the default JSON-LD document loader. */
  278. global $jsonld_default_load_document;
  279. $jsonld_default_load_document = 'jsonld_default_document_loader';
  280. /**
  281. * Sets the default JSON-LD document loader.
  282. *
  283. * @param callable load_document(url) the document loader.
  284. */
  285. function jsonld_set_document_loader($load_document) {
  286. global $jsonld_default_load_document;
  287. $jsonld_default_load_document = $load_document;
  288. }
  289. /**
  290. * Retrieves JSON-LD at the given URL.
  291. *
  292. * @param string $url the URL to retrieve.
  293. *
  294. * @return the JSON-LD.
  295. */
  296. function jsonld_get_url($url) {
  297. global $jsonld_default_load_document;
  298. if($jsonld_default_load_document !== null) {
  299. $document_loader = $jsonld_default_load_document;
  300. } else {
  301. $document_loader = 'jsonld_default_document_loader';
  302. }
  303. $remote_doc = call_user_func($document_loader, $url);
  304. if($remote_doc) {
  305. return $remote_doc->document;
  306. }
  307. return null;
  308. }
  309. /**
  310. * The default implementation to retrieve JSON-LD at the given URL.
  311. *
  312. * @param string $url the URL to to retrieve.
  313. *
  314. * @return stdClass the RemoteDocument object.
  315. */
  316. function jsonld_default_document_loader($url) {
  317. $doc = (object)array(
  318. 'contextUrl' => null, 'document' => null, 'documentUrl' => $url);
  319. $redirects = array();
  320. $opts = array(
  321. 'http' => array(
  322. 'method' => 'GET',
  323. 'header' =>
  324. "Accept: application/ld+json\r\n"),
  325. /* Note: Use jsonld_default_secure_document_loader for security. */
  326. 'ssl' => array(
  327. 'verify_peer' => false,
  328. 'allow_self_signed' => true)
  329. );
  330. $context = stream_context_create($opts);
  331. $content_type = null;
  332. stream_context_set_params($context, array('notification' =>
  333. function($notification_code, $severity, $message) use (
  334. &$redirects, &$content_type) {
  335. switch($notification_code) {
  336. case STREAM_NOTIFY_REDIRECTED:
  337. $redirects[] = $message;
  338. break;
  339. case STREAM_NOTIFY_MIME_TYPE_IS:
  340. $content_type = $message;
  341. break;
  342. };
  343. }));
  344. $result = @file_get_contents($url, false, $context);
  345. if($result === false) {
  346. throw new JsonLdException(
  347. 'Could not retrieve a JSON-LD document from the URL: ' . $url,
  348. 'jsonld.LoadDocumentError', 'loading document failed');
  349. }
  350. $link_header = array();
  351. foreach($http_response_header as $header) {
  352. if(strpos($header, 'link') === 0) {
  353. $value = explode(': ', $header);
  354. if(count($value) > 1) {
  355. $link_header[] = $value[1];
  356. }
  357. }
  358. }
  359. $link_header = jsonld_parse_link_header(join(',', $link_header));
  360. if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
  361. $link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
  362. } else {
  363. $link_header = null;
  364. }
  365. if($link_header && $content_type !== 'application/ld+json') {
  366. // only 1 related link header permitted
  367. if(is_array($link_header)) {
  368. throw new JsonLdException(
  369. 'URL could not be dereferenced, it has more than one ' .
  370. 'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
  371. 'multiple context link headers', array('url' => $url));
  372. }
  373. $doc->{'contextUrl'} = $link_header->target;
  374. }
  375. // update document url based on redirects
  376. $redirs = count($redirects);
  377. if($redirs > 0) {
  378. $url = $redirects[$redirs - 1];
  379. }
  380. $doc->document = $result;
  381. $doc->documentUrl = $url;
  382. return $doc;
  383. }
  384. /**
  385. * The default implementation to retrieve JSON-LD at the given secure URL.
  386. *
  387. * @param string $url the secure URL to to retrieve.
  388. *
  389. * @return stdClass the RemoteDocument object.
  390. */
  391. function jsonld_default_secure_document_loader($url) {
  392. if(strpos($url, 'https') !== 0) {
  393. throw new JsonLdException(
  394. "Could not GET url: '$url'; 'https' is required.",
  395. 'jsonld.LoadDocumentError', 'loading document failed');
  396. }
  397. $doc = (object)array(
  398. 'contextUrl' => null, 'document' => null, 'documentUrl' => $url);
  399. $redirects = array();
  400. // default JSON-LD https GET implementation
  401. $opts = array(
  402. 'http' => array(
  403. 'method' => 'GET',
  404. 'header' =>
  405. "Accept: application/ld+json\r\n"),
  406. 'ssl' => array(
  407. 'verify_peer' => true,
  408. 'allow_self_signed' => false,
  409. 'cafile' => '/etc/ssl/certs/ca-certificates.crt'));
  410. $context = stream_context_create($opts);
  411. $content_type = null;
  412. stream_context_set_params($context, array('notification' =>
  413. function($notification_code, $severity, $message) use (
  414. &$redirects, &$content_type) {
  415. switch($notification_code) {
  416. case STREAM_NOTIFY_REDIRECTED:
  417. $redirects[] = $message;
  418. break;
  419. case STREAM_NOTIFY_MIME_TYPE_IS:
  420. $content_type = $message;
  421. break;
  422. };
  423. }));
  424. $result = @file_get_contents($url, false, $context);
  425. if($result === false) {
  426. throw new JsonLdException(
  427. 'Could not retrieve a JSON-LD document from the URL: ' + $url,
  428. 'jsonld.LoadDocumentError', 'loading document failed');
  429. }
  430. $link_header = array();
  431. foreach($http_response_header as $header) {
  432. if(strpos($header, 'link') === 0) {
  433. $value = explode(': ', $header);
  434. if(count($value) > 1) {
  435. $link_header[] = $value[1];
  436. }
  437. }
  438. }
  439. $link_header = jsonld_parse_link_header(join(',', $link_header));
  440. if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
  441. $link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
  442. } else {
  443. $link_header = null;
  444. }
  445. if($link_header && $content_type !== 'application/ld+json') {
  446. // only 1 related link header permitted
  447. if(is_array($link_header)) {
  448. throw new JsonLdException(
  449. 'URL could not be dereferenced, it has more than one ' .
  450. 'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
  451. 'multiple context link headers', array('url' => $url));
  452. }
  453. $doc->{'contextUrl'} = $link_header->target;
  454. }
  455. // update document url based on redirects
  456. foreach($redirects as $redirect) {
  457. if(strpos($redirect, 'https') !== 0) {
  458. throw new JsonLdException(
  459. "Could not GET redirected url: '$redirect'; 'https' is required.",
  460. 'jsonld.LoadDocumentError', 'loading document failed');
  461. }
  462. $url = $redirect;
  463. }
  464. $doc->document = $result;
  465. $doc->documentUrl = $url;
  466. return $doc;
  467. }
  468. /** Registered global RDF dataset parsers hashed by content-type. */
  469. global $jsonld_rdf_parsers;
  470. $jsonld_rdf_parsers = new stdClass();
  471. /**
  472. * Registers a global RDF dataset parser by content-type, for use with
  473. * jsonld_from_rdf. Global parsers will be used by JsonLdProcessors that do
  474. * not register their own parsers.
  475. *
  476. * @param string $content_type the content-type for the parser.
  477. * @param callable $parser(input) the parser function (takes a string as
  478. * a parameter and returns an RDF dataset).
  479. */
  480. function jsonld_register_rdf_parser($content_type, $parser) {
  481. global $jsonld_rdf_parsers;
  482. $jsonld_rdf_parsers->{$content_type} = $parser;
  483. }
  484. /**
  485. * Unregisters a global RDF dataset parser by content-type.
  486. *
  487. * @param string $content_type the content-type for the parser.
  488. */
  489. function jsonld_unregister_rdf_parser($content_type) {
  490. global $jsonld_rdf_parsers;
  491. if(property_exists($jsonld_rdf_parsers, $content_type)) {
  492. unset($jsonld_rdf_parsers->{$content_type});
  493. }
  494. }
  495. /**
  496. * Parses a URL into its component parts.
  497. *
  498. * @param string $url the URL to parse.
  499. *
  500. * @return assoc the parsed URL.
  501. */
  502. function jsonld_parse_url($url) {
  503. if($url === null) {
  504. $url = '';
  505. }
  506. $keys = array(
  507. 'href', 'protocol', 'scheme', '?authority', 'authority',
  508. '?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path',
  509. '?query', 'query', '?fragment', 'fragment');
  510. $regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/";
  511. preg_match($regex, $url, $match);
  512. $rval = array();
  513. $flags = array();
  514. $len = count($keys);
  515. for($i = 0; $i < $len; ++$i) {
  516. $key = $keys[$i];
  517. if(strpos($key, '?') === 0) {
  518. $flags[substr($key, 1)] = !empty($match[$i]);
  519. } else if(!isset($match[$i])) {
  520. $rval[$key] = null;
  521. } else {
  522. $rval[$key] = $match[$i];
  523. }
  524. }
  525. if(!$flags['authority']) {
  526. $rval['authority'] = null;
  527. }
  528. if(!$flags['auth']) {
  529. $rval['auth'] = $rval['user'] = $rval['pass'] = null;
  530. }
  531. if(!$flags['port']) {
  532. $rval['port'] = null;
  533. }
  534. if(!$flags['query']) {
  535. $rval['query'] = null;
  536. }
  537. if(!$flags['fragment']) {
  538. $rval['fragment'] = null;
  539. }
  540. $rval['normalizedPath'] = jsonld_remove_dot_segments(
  541. $rval['path'], !!$rval['authority']);
  542. return $rval;
  543. }
  544. /**
  545. * Removes dot segments from a URL path.
  546. *
  547. * @param string $path the path to remove dot segments from.
  548. * @param bool $has_authority true if the URL has an authority, false if not.
  549. */
  550. function jsonld_remove_dot_segments($path, $has_authority) {
  551. $rval = '';
  552. if(strpos($path, '/') === 0) {
  553. $rval = '/';
  554. }
  555. // RFC 3986 5.2.4 (reworked)
  556. $input = explode('/', $path);
  557. $output = array();
  558. while(count($input) > 0) {
  559. if($input[0] === '.' || ($input[0] === '' && count($input) > 1)) {
  560. array_shift($input);
  561. continue;
  562. }
  563. if($input[0] === '..') {
  564. array_shift($input);
  565. if($has_authority ||
  566. (count($output) > 0 && $output[count($output) - 1] !== '..')) {
  567. array_pop($output);
  568. } else {
  569. // leading relative URL '..'
  570. $output[] = '..';
  571. }
  572. continue;
  573. }
  574. $output[] = array_shift($input);
  575. }
  576. return $rval . implode('/', $output);
  577. }
  578. /**
  579. * Prepends a base IRI to the given relative IRI.
  580. *
  581. * @param mixed $base a string or the parsed base IRI.
  582. * @param string $iri the relative IRI.
  583. *
  584. * @return string the absolute IRI.
  585. */
  586. function jsonld_prepend_base($base, $iri) {
  587. // skip IRI processing
  588. if($base === null) {
  589. return $iri;
  590. }
  591. // already an absolute IRI
  592. if(strpos($iri, ':') !== false) {
  593. return $iri;
  594. }
  595. // parse base if it is a string
  596. if(is_string($base)) {
  597. $base = jsonld_parse_url($base);
  598. }
  599. // parse given IRI
  600. $rel = jsonld_parse_url($iri);
  601. // per RFC3986 5.2.2
  602. $transform = array('protocol' => $base['protocol']);
  603. if($rel['authority'] !== null) {
  604. $transform['authority'] = $rel['authority'];
  605. $transform['path'] = $rel['path'];
  606. $transform['query'] = $rel['query'];
  607. } else {
  608. $transform['authority'] = $base['authority'];
  609. if($rel['path'] === '') {
  610. $transform['path'] = $base['path'];
  611. if($rel['query'] !== null) {
  612. $transform['query'] = $rel['query'];
  613. } else {
  614. $transform['query'] = $base['query'];
  615. }
  616. } else {
  617. if(strpos($rel['path'], '/') === 0) {
  618. // IRI represents an absolute path
  619. $transform['path'] = $rel['path'];
  620. } else {
  621. // merge paths
  622. $path = $base['path'];
  623. // append relative path to the end of the last directory from base
  624. if($rel['path'] !== '') {
  625. $idx = strrpos($path, '/');
  626. $idx = ($idx === false) ? 0 : $idx + 1;
  627. $path = substr($path, 0, $idx);
  628. if(strlen($path) > 0 && substr($path, -1) !== '/') {
  629. $path .= '/';
  630. }
  631. $path .= $rel['path'];
  632. }
  633. $transform['path'] = $path;
  634. }
  635. $transform['query'] = $rel['query'];
  636. }
  637. }
  638. // remove slashes and dots in path
  639. $transform['path'] = jsonld_remove_dot_segments(
  640. $transform['path'], !!$transform['authority']);
  641. // construct URL
  642. $rval = $transform['protocol'];
  643. if($transform['authority'] !== null) {
  644. $rval .= '//' . $transform['authority'];
  645. }
  646. $rval .= $transform['path'];
  647. if($transform['query'] !== null) {
  648. $rval .= '?' . $transform['query'];
  649. }
  650. if($rel['fragment'] !== null) {
  651. $rval .= '#' . $rel['fragment'];
  652. }
  653. // handle empty base
  654. if($rval === '') {
  655. $rval = './';
  656. }
  657. return $rval;
  658. }
  659. /**
  660. * Removes a base IRI from the given absolute IRI.
  661. *
  662. * @param mixed $base the base IRI.
  663. * @param string $iri the absolute IRI.
  664. *
  665. * @return string the relative IRI if relative to base, otherwise the absolute
  666. * IRI.
  667. */
  668. function jsonld_remove_base($base, $iri) {
  669. // skip IRI processing
  670. if($base === null) {
  671. return $iri;
  672. }
  673. if(is_string($base)) {
  674. $base = jsonld_parse_url($base);
  675. }
  676. // establish base root
  677. $root = '';
  678. if($base['href'] !== '') {
  679. $root .= "{$base['protocol']}//{$base['authority']}";
  680. } else if(strpos($iri, '//') === false) {
  681. // support network-path reference with empty base
  682. $root .= '//';
  683. }
  684. // IRI not relative to base
  685. if($root === '' || strpos($iri, $root) !== 0) {
  686. return $iri;
  687. }
  688. // remove root from IRI
  689. $rel = jsonld_parse_url(substr($iri, strlen($root)));
  690. // remove path segments that match (do not remove last segment unless there
  691. // is a hash or query)
  692. $base_segments = explode('/', $base['normalizedPath']);
  693. $iri_segments = explode('/', $rel['normalizedPath']);
  694. $last = ($rel['query'] || $rel['fragment']) ? 0 : 1;
  695. while(count($base_segments) > 0 && count($iri_segments) > $last) {
  696. if($base_segments[0] !== $iri_segments[0]) {
  697. break;
  698. }
  699. array_shift($base_segments);
  700. array_shift($iri_segments);
  701. }
  702. // use '../' for each non-matching base segment
  703. $rval = '';
  704. if(count($base_segments) > 0) {
  705. // don't count the last segment (if it ends with '/' last path doesn't
  706. // count and if it doesn't end with '/' it isn't a path)
  707. array_pop($base_segments);
  708. foreach($base_segments as $segment) {
  709. $rval .= '../';
  710. }
  711. }
  712. // prepend remaining segments
  713. $rval .= implode('/', $iri_segments);
  714. // add query and hash
  715. if($rel['query'] !== null) {
  716. $rval .= "?{$rel['query']}";
  717. }
  718. if($rel['fragment'] !== null) {
  719. $rval .= "#{$rel['fragment']}";
  720. }
  721. if($rval === '') {
  722. $rval = './';
  723. }
  724. return $rval;
  725. }
  726. /**
  727. * A JSON-LD processor.
  728. */
  729. class JsonLdProcessor {
  730. /** XSD constants */
  731. const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
  732. const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
  733. const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
  734. const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
  735. /** RDF constants */
  736. const RDF_LIST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#List';
  737. const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
  738. const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest';
  739. const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil';
  740. const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
  741. const RDF_LANGSTRING =
  742. 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString';
  743. /** Restraints */
  744. const MAX_CONTEXT_URLS = 10;
  745. /** Processor-specific RDF dataset parsers. */
  746. protected $rdfParsers = null;
  747. /**
  748. * Constructs a JSON-LD processor.
  749. */
  750. public function __construct() {}
  751. /**
  752. * Performs JSON-LD compaction.
  753. *
  754. * @param mixed $input the JSON-LD object to compact.
  755. * @param mixed $ctx the context to compact with.
  756. * @param assoc $options the compaction options.
  757. * [base] the base IRI to use.
  758. * [compactArrays] true to compact arrays to single values when
  759. * appropriate, false not to (default: true).
  760. * [graph] true to always output a top-level graph (default: false).
  761. * [skipExpansion] true to assume the input is expanded and skip
  762. * expansion, false not to, defaults to false.
  763. * [activeCtx] true to also return the active context used.
  764. * [documentLoader(url)] the document loader.
  765. *
  766. * @return mixed the compacted JSON-LD output.
  767. */
  768. public function compact($input, $ctx, $options) {
  769. global $jsonld_default_load_document;
  770. if($ctx === null) {
  771. throw new JsonLdException(
  772. 'The compaction context must not be null.',
  773. 'jsonld.CompactError', 'invalid local context');
  774. }
  775. // nothing to compact
  776. if($input === null) {
  777. return null;
  778. }
  779. self::setdefaults($options, array(
  780. 'base' => is_string($input) ? $input : '',
  781. 'compactArrays' => true,
  782. 'graph' => false,
  783. 'skipExpansion' => false,
  784. 'activeCtx' => false,
  785. 'documentLoader' => $jsonld_default_load_document,
  786. 'link' => false));
  787. if($options['link']) {
  788. // force skip expansion when linking, "link" is not part of the
  789. // public API, it should only be called from framing
  790. $options['skipExpansion'] = true;
  791. }
  792. if($options['skipExpansion'] === true) {
  793. $expanded = $input;
  794. } else {
  795. // expand input
  796. try {
  797. $expanded = $this->expand($input, $options);
  798. } catch(JsonLdException $e) {
  799. throw new JsonLdException(
  800. 'Could not expand input before compaction.',
  801. 'jsonld.CompactError', null, null, $e);
  802. }
  803. }
  804. // process context
  805. $active_ctx = $this->_getInitialContext($options);
  806. try {
  807. $active_ctx = $this->processContext($active_ctx, $ctx, $options);
  808. } catch(JsonLdException $e) {
  809. throw new JsonLdException(
  810. 'Could not process context before compaction.',
  811. 'jsonld.CompactError', null, null, $e);
  812. }
  813. // do compaction
  814. $compacted = $this->_compact($active_ctx, null, $expanded, $options);
  815. if($options['compactArrays'] &&
  816. !$options['graph'] && is_array($compacted)) {
  817. if(count($compacted) === 1) {
  818. // simplify to a single item
  819. $compacted = $compacted[0];
  820. } else if(count($compacted) === 0) {
  821. // simplify to an empty object
  822. $compacted = new stdClass();
  823. }
  824. } else if($options['graph']) {
  825. // always use array if graph option is on
  826. $compacted = self::arrayify($compacted);
  827. }
  828. // follow @context key
  829. if(is_object($ctx) && property_exists($ctx, '@context')) {
  830. $ctx = $ctx->{'@context'};
  831. }
  832. // build output context
  833. $ctx = self::copy($ctx);
  834. $ctx = self::arrayify($ctx);
  835. // remove empty contexts
  836. $tmp = $ctx;
  837. $ctx = array();
  838. foreach($tmp as $v) {
  839. if(!is_object($v) || count(array_keys((array)$v)) > 0) {
  840. $ctx[] = $v;
  841. }
  842. }
  843. // remove array if only one context
  844. $ctx_length = count($ctx);
  845. $has_context = ($ctx_length > 0);
  846. if($ctx_length === 1) {
  847. $ctx = $ctx[0];
  848. }
  849. // add context and/or @graph
  850. if(is_array($compacted)) {
  851. // use '@graph' keyword
  852. $kwgraph = $this->_compactIri($active_ctx, '@graph');
  853. $graph = $compacted;
  854. $compacted = new stdClass();
  855. if($has_context) {
  856. $compacted->{'@context'} = $ctx;
  857. }
  858. $compacted->{$kwgraph} = $graph;
  859. } else if(is_object($compacted) && $has_context) {
  860. // reorder keys so @context is first
  861. $graph = $compacted;
  862. $compacted = new stdClass();
  863. $compacted->{'@context'} = $ctx;
  864. foreach($graph as $k => $v) {
  865. $compacted->{$k} = $v;
  866. }
  867. }
  868. if($options['activeCtx']) {
  869. return array('compacted' => $compacted, 'activeCtx' => $active_ctx);
  870. }
  871. return $compacted;
  872. }
  873. /**
  874. * Performs JSON-LD expansion.
  875. *
  876. * @param mixed $input the JSON-LD object to expand.
  877. * @param assoc $options the options to use:
  878. * [base] the base IRI to use.
  879. * [expandContext] a context to expand with.
  880. * [keepFreeFloatingNodes] true to keep free-floating nodes,
  881. * false not to, defaults to false.
  882. * [documentLoader(url)] the document loader.
  883. *
  884. * @return array the expanded JSON-LD output.
  885. */
  886. public function expand($input, $options) {
  887. global $jsonld_default_load_document;
  888. self::setdefaults($options, array(
  889. 'keepFreeFloatingNodes' => false,
  890. 'documentLoader' => $jsonld_default_load_document));
  891. // if input is a string, attempt to dereference remote document
  892. if(is_string($input)) {
  893. $remote_doc = call_user_func($options['documentLoader'], $input);
  894. } else {
  895. $remote_doc = (object)array(
  896. 'contextUrl' => null,
  897. 'documentUrl' => null,
  898. 'document' => $input);
  899. }
  900. try {
  901. if($remote_doc->document === null) {
  902. throw new JsonLdException(
  903. 'No remote document found at the given URL.',
  904. 'jsonld.NullRemoteDocument');
  905. }
  906. if(is_string($remote_doc->document)) {
  907. $remote_doc->document = self::_parse_json($remote_doc->document);
  908. }
  909. } catch(Exception $e) {
  910. throw new JsonLdException(
  911. 'Could not retrieve a JSON-LD document from the URL.',
  912. 'jsonld.LoadDocumentError', 'loading document failed',
  913. array('remoteDoc' => $remote_doc), $e);
  914. }
  915. // set default base
  916. self::setdefault($options, 'base', $remote_doc->documentUrl ?: '');
  917. // build meta-object and retrieve all @context urls
  918. $input = (object)array(
  919. 'document' => self::copy($remote_doc->document),
  920. 'remoteContext' => (object)array(
  921. '@context' => $remote_doc->contextUrl));
  922. if(isset($options['expandContext'])) {
  923. $expand_context = self::copy($options['expandContext']);
  924. if(is_object($expand_context) &&
  925. property_exists($expand_context, '@context')) {
  926. $input->expandContext = $expand_context;
  927. } else {
  928. $input->expandContext = (object)array('@context' => $expand_context);
  929. }
  930. }
  931. // retrieve all @context URLs in the input
  932. try {
  933. $this->_retrieveContextUrls(
  934. $input, new stdClass(), $options['documentLoader'], $options['base']);
  935. } catch(Exception $e) {
  936. throw new JsonLdException(
  937. 'Could not perform JSON-LD expansion.',
  938. 'jsonld.ExpandError', null, null, $e);
  939. }
  940. $active_ctx = $this->_getInitialContext($options);
  941. $document = $input->document;
  942. $remote_context = $input->remoteContext->{'@context'};
  943. // process optional expandContext
  944. if(property_exists($input, 'expandContext')) {
  945. $active_ctx = self::_processContext(
  946. $active_ctx, $input->expandContext, $options);
  947. }
  948. // process remote context from HTTP Link Header
  949. if($remote_context) {
  950. $active_ctx = self::_processContext(
  951. $active_ctx, $remote_context, $options);
  952. }
  953. // do expansion
  954. $expanded = $this->_expand($active_ctx, null, $document, $options, false);
  955. // optimize away @graph with no other properties
  956. if(is_object($expanded) && property_exists($expanded, '@graph') &&
  957. count(array_keys((array)$expanded)) === 1) {
  958. $expanded = $expanded->{'@graph'};
  959. } else if($expanded === null) {
  960. $expanded = array();
  961. }
  962. // normalize to an array
  963. return self::arrayify($expanded);
  964. }
  965. /**
  966. * Performs JSON-LD flattening.
  967. *
  968. * @param mixed $input the JSON-LD to flatten.
  969. * @param ctx the context to use to compact the flattened output, or null.
  970. * @param assoc $options the options to use:
  971. * [base] the base IRI to use.
  972. * [expandContext] a context to expand with.
  973. * [documentLoader(url)] the document loader.
  974. *
  975. * @return array the flattened output.
  976. */
  977. public function flatten($input, $ctx, $options) {
  978. global $jsonld_default_load_document;
  979. self::setdefaults($options, array(
  980. 'base' => is_string($input) ? $input : '',
  981. 'documentLoader' => $jsonld_default_load_document));
  982. try {
  983. // expand input
  984. $expanded = $this->expand($input, $options);
  985. } catch(Exception $e) {
  986. throw new JsonLdException(
  987. 'Could not expand input before flattening.',
  988. 'jsonld.FlattenError', null, null, $e);
  989. }
  990. // do flattening
  991. $flattened = $this->_flatten($expanded);
  992. if($ctx === null) {
  993. return $flattened;
  994. }
  995. // compact result (force @graph option to true, skip expansion)
  996. $options['graph'] = true;
  997. $options['skipExpansion'] = true;
  998. try {
  999. $compacted = $this->compact($flattened, $ctx, $options);
  1000. } catch(Exception $e) {
  1001. throw new JsonLdException(
  1002. 'Could not compact flattened output.',
  1003. 'jsonld.FlattenError', null, null, $e);
  1004. }
  1005. return $compacted;
  1006. }
  1007. /**
  1008. * Performs JSON-LD framing.
  1009. *
  1010. * @param mixed $input the JSON-LD object to frame.
  1011. * @param stdClass $frame the JSON-LD frame to use.
  1012. * @param $options the framing options.
  1013. * [base] the base IRI to use.
  1014. * [expandContext] a context to expand with.
  1015. * [embed] default @embed flag: '@last', '@always', '@never', '@link'
  1016. * (default: '@last').
  1017. * [explicit] default @explicit flag (default: false).
  1018. * [requireAll] default @requireAll flag (default: true).
  1019. * [omitDefault] default @omitDefault flag (default: false).
  1020. * [documentLoader(url)] the document loader.
  1021. *
  1022. * @return stdClass the framed JSON-LD output.
  1023. */
  1024. public function frame($input, $frame, $options) {
  1025. global $jsonld_default_load_document;
  1026. self::setdefaults($options, array(
  1027. 'base' => is_string($input) ? $input : '',
  1028. 'compactArrays' => true,
  1029. 'embed' => '@last',
  1030. 'explicit' => false,
  1031. 'requireAll' => true,
  1032. 'omitDefault' => false,
  1033. 'documentLoader' => $jsonld_default_load_document));
  1034. // if frame is a string, attempt to dereference remote document
  1035. if(is_string($frame)) {
  1036. $remote_frame = call_user_func($options['documentLoader'], $frame);
  1037. } else {
  1038. $remote_frame = (object)array(
  1039. 'contextUrl' => null,
  1040. 'documentUrl' => null,
  1041. 'document' => $frame);
  1042. }
  1043. try {
  1044. if($remote_frame->document === null) {
  1045. throw new JsonLdException(
  1046. 'No remote document found at the given URL.',
  1047. 'jsonld.NullRemoteDocument');
  1048. }
  1049. if(is_string($remote_frame->document)) {
  1050. $remote_frame->document = self::_parse_json($remote_frame->document);
  1051. }
  1052. } catch(Exception $e) {
  1053. throw new JsonLdException(
  1054. 'Could not retrieve a JSON-LD document from the URL.',
  1055. 'jsonld.LoadDocumentError', 'loading document failed',
  1056. array('remoteDoc' => $remote_frame), $e);
  1057. }
  1058. // preserve frame context
  1059. $frame = $remote_frame->document;
  1060. if($frame !== null) {
  1061. $ctx = (property_exists($frame, '@context') ?
  1062. $frame->{'@context'} : new stdClass());
  1063. if($remote_frame->contextUrl !== null) {
  1064. if($ctx !== null) {
  1065. $ctx = $remote_frame->contextUrl;
  1066. } else {
  1067. $ctx = self::arrayify($ctx);
  1068. $ctx[] = $remote_frame->contextUrl;
  1069. }
  1070. $frame->{'@context'} = $ctx;
  1071. }
  1072. }
  1073. try {
  1074. // expand input
  1075. $expanded = $this->expand($input, $options);
  1076. } catch(Exception $e) {
  1077. throw new JsonLdException(
  1078. 'Could not expand input before framing.',
  1079. 'jsonld.FrameError', null, null, $e);
  1080. }
  1081. try {
  1082. // expand frame
  1083. $opts = $options;
  1084. $opts['keepFreeFloatingNodes'] = true;
  1085. $expanded_frame = $this->expand($frame, $opts);
  1086. } catch(Exception $e) {
  1087. throw new JsonLdException(
  1088. 'Could not expand frame before framing.',
  1089. 'jsonld.FrameError', null, null, $e);
  1090. }
  1091. // do framing
  1092. $framed = $this->_frame($expanded, $expanded_frame, $options);
  1093. try {
  1094. // compact result (force @graph option to true, skip expansion, check
  1095. // for linked embeds)
  1096. $options['graph'] = true;
  1097. $options['skipExpansion'] = true;
  1098. $options['link'] = new ArrayObject();
  1099. $options['activeCtx'] = true;
  1100. $result = $this->compact($framed, $ctx, $options);
  1101. } catch(Exception $e) {
  1102. throw new JsonLdException(
  1103. 'Could not compact framed output.',
  1104. 'jsonld.FrameError', null, null, $e);
  1105. }
  1106. $compacted = $result['compacted'];
  1107. $active_ctx = $result['activeCtx'];
  1108. // get graph alias
  1109. $graph = $this->_compactIri($active_ctx, '@graph');
  1110. // remove @preserve from results
  1111. $options['link'] = new ArrayObject();
  1112. $compacted->{$graph} = $this->_removePreserve(
  1113. $active_ctx, $compacted->{$graph}, $options);
  1114. return $compacted;
  1115. }
  1116. /**
  1117. * Performs JSON-LD normalization.
  1118. *
  1119. * @param mixed $input the JSON-LD object to normalize.
  1120. * @param assoc $options the options to use:
  1121. * [base] the base IRI to use.
  1122. * [expandContext] a context to expand with.
  1123. * [inputFormat] the format if input is not JSON-LD:
  1124. * 'application/nquads' for N-Quads.
  1125. * [format] the format if output is a string:
  1126. * 'application/nquads' for N-Quads.
  1127. * [documentLoader(url)] the document loader.
  1128. *
  1129. * @return mixed the normalized output.
  1130. */
  1131. public function normalize($input, $options) {
  1132. global $jsonld_default_load_document;
  1133. self::setdefaults($options, array(
  1134. 'base' => is_string($input) ? $input : '',
  1135. 'documentLoader' => $jsonld_default_load_document));
  1136. if(isset($options['inputFormat'])) {
  1137. if($options['inputFormat'] != 'application/nquads') {
  1138. throw new JsonLdException(
  1139. 'Unknown normalization input format.', 'jsonld.NormalizeError');
  1140. }
  1141. $dataset = $this->parseNQuads($input);
  1142. } else {
  1143. try {
  1144. // convert to RDF dataset then do normalization
  1145. $opts = $options;
  1146. if(isset($opts['format'])) {
  1147. unset($opts['format']);
  1148. }
  1149. $opts['produceGeneralizedRdf'] = false;
  1150. $dataset = $this->toRDF($input, $opts);
  1151. } catch(Exception $e) {
  1152. throw new JsonLdException(
  1153. 'Could not convert input to RDF dataset before normalization.',
  1154. 'jsonld.NormalizeError', null, null, $e);
  1155. }
  1156. }
  1157. // do normalization
  1158. return $this->_normalize($dataset, $options);
  1159. }
  1160. /**
  1161. * Converts an RDF dataset to JSON-LD.
  1162. *
  1163. * @param mixed $dataset a serialized string of RDF in a format specified
  1164. * by the format option or an RDF dataset to convert.
  1165. * @param assoc $options the options to use:
  1166. * [format] the format if input is a string:
  1167. * 'application/nquads' for N-Quads (default).
  1168. * [useRdfType] true to use rdf:type, false to use @type
  1169. * (default: false).
  1170. * [useNativeTypes] true to convert XSD types into native types
  1171. * (boolean, integer, double), false not to (default: false).
  1172. *
  1173. * @return array the JSON-LD output.
  1174. */
  1175. public function fromRDF($dataset, $options) {
  1176. global $jsonld_rdf_parsers;
  1177. self::setdefaults($options, array(
  1178. 'useRdfType' => false,
  1179. 'useNativeTypes' => false));
  1180. if(!isset($options['format']) && is_string($dataset)) {
  1181. // set default format to nquads
  1182. $options['format'] = 'application/nquads';
  1183. }
  1184. // handle special format
  1185. if(isset($options['format']) && $options['format']) {
  1186. // supported formats (processor-specific and global)
  1187. if(($this->rdfParsers !== null &&
  1188. !property_exists($this->rdfParsers, $options['format'])) ||
  1189. $this->rdfParsers === null &&
  1190. !property_exists($jsonld_rdf_parsers, $options['format'])) {
  1191. throw new JsonLdException(
  1192. 'Unknown input format.',
  1193. 'jsonld.UnknownFormat', null, array('format' => $options['format']));
  1194. }
  1195. if($this->rdfParsers !== null) {
  1196. $callable = $this->rdfParsers->{$options['format']};
  1197. } else {
  1198. $callable = $jsonld_rdf_parsers->{$options['format']};
  1199. }
  1200. $dataset = call_user_func($callable, $dataset);
  1201. }
  1202. // convert from RDF
  1203. return $this->_fromRDF($dataset, $options);
  1204. }
  1205. /**
  1206. * Outputs the RDF dataset found in the given JSON-LD object.
  1207. *
  1208. * @param mixed $input the JSON-LD object.
  1209. * @param assoc $options the options to use:
  1210. * [base] the base IRI to use.
  1211. * [expandContext] a context to expand with.
  1212. * [format] the format to use to output a string:
  1213. * 'application/nquads' for N-Quads.
  1214. * [produceGeneralizedRdf] true to output generalized RDF, false
  1215. * to produce only standard RDF (default: false).
  1216. * [documentLoader(url)] the document loader.
  1217. *
  1218. * @return mixed the resulting RDF dataset (or a serialization of it).
  1219. */
  1220. public function toRDF($input, $options) {
  1221. global $jsonld_default_load_document;
  1222. self::setdefaults($options, array(
  1223. 'base' => is_string($input) ? $input : '',
  1224. 'produceGeneralizedRdf' => false,
  1225. 'documentLoader' => $jsonld_default_load_document));
  1226. try {
  1227. // expand input
  1228. $expanded = $this->expand($input, $options);
  1229. } catch(JsonLdException $e) {
  1230. throw new JsonLdException(
  1231. 'Could not expand input before serialization to RDF.',
  1232. 'jsonld.RdfError', null, null, $e);
  1233. }
  1234. // create node map for default graph (and any named graphs)
  1235. $namer = new UniqueNamer('_:b');
  1236. $node_map = (object)array('@default' => new stdClass());
  1237. $this->_createNodeMap($expanded, $node_map, '@default', $namer);
  1238. // output RDF dataset
  1239. $dataset = new stdClass();
  1240. $graph_names = array_keys((array)$node_map);
  1241. sort($graph_names);
  1242. foreach($graph_names as $graph_name) {
  1243. $graph = $node_map->{$graph_name};
  1244. // skip relative IRIs
  1245. if($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) {
  1246. $dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options);
  1247. }
  1248. }
  1249. $rval = $dataset;
  1250. // convert to output format
  1251. if(isset($options['format']) && $options['format']) {
  1252. // supported formats
  1253. if($options['format'] === 'application/nquads') {
  1254. $rval = self::toNQuads($dataset);
  1255. } else {
  1256. throw new JsonLdException(
  1257. 'Unknown output format.', 'jsonld.UnknownFormat',
  1258. null, array('format' => $options['format']));
  1259. }
  1260. }
  1261. return $rval;
  1262. }
  1263. /**
  1264. * Processes a local context, resolving any URLs as necessary, and returns a
  1265. * new active context in its callback.
  1266. *
  1267. * @param stdClass $active_ctx the current active context.
  1268. * @param mixed $local_ctx the local context to process.
  1269. * @param assoc $options the options to use:
  1270. * [documentLoader(url)] the document loader.
  1271. *
  1272. * @return stdClass the new active context.
  1273. */
  1274. public function processContext($active_ctx, $local_ctx, $options) {
  1275. global $jsonld_default_load_document;
  1276. self::setdefaults($options, array(
  1277. 'base' => '',
  1278. 'documentLoader' => $jsonld_default_load_document));
  1279. // return initial context early for null context
  1280. if($local_ctx === null) {
  1281. return $this->_getInitialContext($options);
  1282. }
  1283. // retrieve URLs in local_ctx
  1284. $local_ctx = self::copy($local_ctx);
  1285. if(is_string($local_ctx) or (
  1286. is_object($local_ctx) && !property_exists($local_ctx, '@context'))) {
  1287. $local_ctx = (object)array('@context' => $local_ctx);
  1288. }
  1289. try {
  1290. $this->_retrieveContextUrls(
  1291. $local_ctx, new stdClass(),
  1292. $options['documentLoader'], $options['base']);
  1293. } catch(Exception $e) {
  1294. throw new JsonLdException(
  1295. 'Could not process JSON-LD context.',
  1296. 'jsonld.ContextError', null, null, $e);
  1297. }
  1298. // process context
  1299. return $this->_processContext($active_ctx, $local_ctx, $options);
  1300. }
  1301. /**
  1302. * Returns true if the given subject has the given property.
  1303. *
  1304. * @param stdClass $subject the subject to check.
  1305. * @param string $property the property to look for.
  1306. *
  1307. * @return bool true if the subject has the given property, false if not.
  1308. */
  1309. public static function hasProperty($subject, $property) {
  1310. $rval = false;
  1311. if(property_exists($subject, $property)) {
  1312. $value = $subject->{$property};
  1313. $rval = (!is_array($value) || count($value) > 0);
  1314. }
  1315. return $rval;
  1316. }
  1317. /**
  1318. * Determines if the given value is a property of the given subject.
  1319. *
  1320. * @param stdClass $subject the subject to check.
  1321. * @param string $property the property to check.
  1322. * @param mixed $value the value to check.
  1323. *
  1324. * @return bool true if the value exists, false if not.
  1325. */
  1326. public static function hasValue($subject, $property, $value) {
  1327. $rval = false;
  1328. if(self::hasProperty($subject, $property)) {
  1329. $val = $subject->{$property};
  1330. $is_list = self::_isList($val);
  1331. if(is_array($val) || $is_list) {
  1332. if($is_list) {
  1333. $val = $val->{'@list'};
  1334. }
  1335. foreach($val as $v) {
  1336. if(self::compareValues($value, $v)) {
  1337. $rval = true;
  1338. break;
  1339. }
  1340. }
  1341. } else if(!is_array($value)) {
  1342. // avoid matching the set of values with an array value parameter
  1343. $rval = self::compareValues($value, $val);
  1344. }
  1345. }
  1346. return $rval;
  1347. }
  1348. /**
  1349. * Adds a value to a subject. If the value is an array, all values in the
  1350. * array will be added.
  1351. *
  1352. * Note: If the value is a subject that already exists as a property of the
  1353. * given subject, this method makes no attempt to deeply merge properties.
  1354. * Instead, the value will not be added.
  1355. *
  1356. * @param stdClass $subject the subject to add the value to.
  1357. * @param string $property the property that relates the value to the subject.
  1358. * @param mixed $value the value to add.
  1359. * @param assoc [$options] the options to use:
  1360. * [propertyIsArray] true if the property is always an array, false
  1361. * if not (default: false).
  1362. * [allowDuplicate] true to allow duplicates, false not to (uses a
  1363. * simple shallow comparison of subject ID or value)
  1364. * (default: true).
  1365. */
  1366. public static function addValue(
  1367. $subject, $property, $value, $options=array()) {
  1368. self::setdefaults($options, array(
  1369. 'allowDuplicate' => true,
  1370. 'propertyIsArray' => false));
  1371. if(is_array($value)) {
  1372. if(count($value) === 0 && $options['propertyIsArray'] &&
  1373. !property_exists($subject, $property)) {
  1374. $subject->{$property} = array();
  1375. }
  1376. foreach($value as $v) {
  1377. self::addValue($subject, $property, $v, $options);
  1378. }
  1379. } else if(property_exists($subject, $property)) {
  1380. // check if subject already has value if duplicates not allowed
  1381. $has_value = (!$options['allowDuplicate'] &&
  1382. self::hasValue($subject, $property, $value));
  1383. // make property an array if value not present or always an array
  1384. if(!is_array($subject->{$property}) &&
  1385. (!$has_value || $options['propertyIsArray'])) {
  1386. $subject->{$property} = array($subject->{$property});
  1387. }
  1388. // add new value
  1389. if(!$has_value) {
  1390. $subject->{$property}[] = $value;
  1391. }
  1392. } else {
  1393. // add new value as set or single value
  1394. $subject->{$property} = ($options['propertyIsArray'] ?
  1395. array($value) : $value);
  1396. }
  1397. }
  1398. /**
  1399. * Gets all of the values for a subject's property as an array.
  1400. *
  1401. * @param stdClass $subject the subject.
  1402. * @param string $property the property.
  1403. *
  1404. * @return array all of the values for a subject's property as an array.
  1405. */
  1406. public static function getValues($subject, $property) {
  1407. $rval = (property_exists($subject, $property) ?
  1408. $subject->{$property} : array());
  1409. return self::arrayify($rval);
  1410. }
  1411. /**
  1412. * Removes a property from a subject.
  1413. *
  1414. * @param stdClass $subject the subject.
  1415. * @param string $property the property.
  1416. */
  1417. public static function removeProperty($subject, $property) {
  1418. unset($subject->{$property});
  1419. }
  1420. /**
  1421. * Removes a value from a subject.
  1422. *
  1423. * @param stdClass $subject the subject.
  1424. * @param string $property the property that relates the value to the subject.
  1425. * @param mixed $value the value to remove.
  1426. * @param assoc [$options] the options to use:
  1427. * [propertyIsArray] true if the property is always an array,
  1428. * false if not (default: false).
  1429. */
  1430. public static function removeValue(
  1431. $subject, $property, $value, $options=array()) {
  1432. self::setdefaults($options, array(
  1433. 'propertyIsArray' => false));
  1434. // filter out value
  1435. $filter = function($e) use ($value) {
  1436. return !self::compareValues($e, $value);
  1437. };
  1438. $values = self::getValues($subject, $property);
  1439. $values = array_values(array_filter($values, $filter));
  1440. if(count($values) === 0) {
  1441. self::removeProperty($subject, $property);
  1442. } else if(count($values) === 1 && !$options['propertyIsArray']) {
  1443. $subject->{$property} = $values[0];
  1444. } else {
  1445. $subject->{$property} = $values;
  1446. }
  1447. }
  1448. /**
  1449. * Compares two JSON-LD values for equality. Two JSON-LD values will be
  1450. * considered equal if:
  1451. *
  1452. * 1. They are both primitives of the same type and value.
  1453. * 2. They are both @values with the same @value, @type, @language,
  1454. * and @index, OR
  1455. * 3. They both have @ids that are the same.
  1456. *
  1457. * @param mixed $v1 the first value.
  1458. * @param mixed $v2 the second value.
  1459. *
  1460. * @return bool true if v1 and v2 are considered equal, false if not.
  1461. */
  1462. public static function compareValues($v1, $v2) {
  1463. // 1. equal primitives
  1464. if($v1 === $v2) {
  1465. return true;
  1466. }
  1467. // 2. equal @values
  1468. if(self::_isValue($v1) && self::_isValue($v2)) {
  1469. return (
  1470. self::_compareKeyValues($v1, $v2, '@value') &&
  1471. self::_compareKeyValues($v1, $v2, '@type') &&
  1472. self::_compareKeyValues($v1, $v2, '@language') &&
  1473. self::_compareKeyValues($v1, $v2, '@index'));
  1474. }
  1475. // 3. equal @ids
  1476. if(is_object($v1) && property_exists($v1, '@id') &&
  1477. is_object($v2) && property_exists($v2, '@id')) {
  1478. return $v1->{'@id'} === $v2->{'@id'};
  1479. }
  1480. return false;
  1481. }
  1482. /**
  1483. * Gets the value for the given active context key and type, null if none is
  1484. * set.
  1485. *
  1486. * @param stdClass $ctx the active context.
  1487. * @param string $key the context key.
  1488. * @param string [$type] the type of value to get (eg: '@id', '@type'), if not
  1489. * specified gets the entire entry for a key, null if not found.
  1490. *
  1491. * @return mixed the value.
  1492. */
  1493. public static function getContextValue($ctx, $key, $type) {
  1494. $rval = null;
  1495. // return null for invalid key
  1496. if($key === null) {
  1497. return $rval;
  1498. }
  1499. // get default language
  1500. if($type === '@language' && property_exists($ctx, $type)) {
  1501. $rval = $ctx->{$type};
  1502. }
  1503. // get specific entry information
  1504. if(property_exists($ctx->mappings, $key)) {
  1505. $entry = $ctx->mappings->{$key};
  1506. if($entry === null) {
  1507. return null;
  1508. }
  1509. if($type === null) {
  1510. // return whole entry
  1511. $rval = $entry;
  1512. } else if(property_exists($entry, $type)) {
  1513. // return entry value for type
  1514. $rval = $entry->{$type};
  1515. }
  1516. }
  1517. return $rval;
  1518. }
  1519. /**
  1520. * Parses RDF in the form of N-Quads.
  1521. *
  1522. * @param string $input the N-Quads input to parse.
  1523. *
  1524. * @return stdClass an RDF dataset.
  1525. */
  1526. public static function parseNQuads($input) {
  1527. // define partial regexes
  1528. $iri = '(?:<([^:]+:[^>]*)>)';
  1529. $bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))';
  1530. $plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"';
  1531. $datatype = "(?:\\^\\^$iri)";
  1532. $language = '(?:@([a-z]+(?:-[a-z0-9]+)*))';
  1533. $literal = "(?:$plain(?:$datatype|$language)?)";
  1534. $ws = '[ \\t]';
  1535. $eoln = '/(?:\r\n)|(?:\n)|(?:\r)/';
  1536. $empty = "/^$ws*$/";
  1537. // define quad part regexes
  1538. $subject = "(?:$iri|$bnode)$ws+";
  1539. $property = "$iri$ws+";
  1540. $object = "(?:$iri|$bnode|$literal)$ws*";
  1541. $graph_name = "(?:\\.|(?:(?:$iri|$bnode)$ws*\\.))";
  1542. // full quad regex
  1543. $quad = "/^$ws*$subject$property$object$graph_name$ws*$/";
  1544. // build RDF dataset
  1545. $dataset = new stdClass();
  1546. // split N-Quad input into lines
  1547. $lines = preg_split($eoln, $input);
  1548. $line_number = 0;
  1549. foreach($lines as $line) {
  1550. $line_number += 1;
  1551. // skip empty lines
  1552. if(preg_match($empty, $line)) {
  1553. continue;
  1554. }
  1555. // parse quad
  1556. if(!preg_match($quad, $line, $match)) {
  1557. throw new JsonLdException(
  1558. 'Error while parsing N-Quads; invalid quad.',
  1559. 'jsonld.ParseError', null, array('line' => $line_number));
  1560. }
  1561. // create RDF triple
  1562. $triple = (object)array(
  1563. 'subject' => new stdClass(),
  1564. 'predicate' => new stdClass(),
  1565. 'object' => new stdClass());
  1566. // get subject
  1567. if($match[1] !== '') {
  1568. $triple->subject->type = 'IRI';
  1569. $triple->subject->value = $match[1];
  1570. } else {
  1571. $triple->subject->type = 'blank node';
  1572. $triple->subject->value = $match[2];
  1573. }
  1574. // get predicate
  1575. $triple->predicate->type = 'IRI';
  1576. $triple->predicate->value = $match[3];
  1577. // get object
  1578. if($match[4] !== '') {
  1579. $triple->object->type = 'IRI';
  1580. $triple->object->value = $match[4];
  1581. } else if($match[5] !== '') {
  1582. $triple->object->type = 'blank node';
  1583. $triple->object->value = $match[5];
  1584. } else {
  1585. $triple->object->type = 'literal';
  1586. $unescaped = str_replace(
  1587. array('\"', '\t', '\n', '\r', '\\\\'),
  1588. array('"', "\t", "\n", "\r", '\\'),
  1589. $match[6]);
  1590. if(isset($match[7]) && $match[7] !== '') {
  1591. $triple->object->datatype = $match[7];
  1592. } else if(isset($match[8]) && $match[8] !== '') {
  1593. $triple->object->datatype = self::RDF_LANGSTRING;
  1594. $triple->object->language = $match[8];
  1595. } else {
  1596. $triple->object->datatype = self::XSD_STRING;
  1597. }
  1598. $triple->object->value = $unescaped;
  1599. }
  1600. // get graph name ('@default' is used for the default graph)
  1601. $name = '@default';
  1602. if(isset($match[9]) && $match[9] !== '') {
  1603. $name = $match[9];
  1604. } else if(isset($match[10]) && $match[10] !== '') {
  1605. $name = $match[10];
  1606. }
  1607. // initialize graph in dataset
  1608. if(!property_exists($dataset, $name)) {
  1609. $dataset->{$name} = array($triple);
  1610. } else {
  1611. // add triple if unique to its graph
  1612. $unique = true;
  1613. $triples = &$dataset->{$name};
  1614. foreach($triples as $t) {
  1615. if(self::_compareRDFTriples($t, $triple)) {
  1616. $unique = false;
  1617. break;
  1618. }
  1619. }
  1620. if($unique) {
  1621. $triples[] = $triple;
  1622. }
  1623. }
  1624. }
  1625. return $dataset;
  1626. }
  1627. /**
  1628. * Converts an RDF dataset to N-Quads.
  1629. *
  1630. * @param stdClass $dataset the RDF dataset to convert.
  1631. *
  1632. * @return string the N-Quads string.
  1633. */
  1634. public static function toNQuads($dataset) {
  1635. $quads = array();
  1636. foreach($dataset as $graph_name => $triples) {
  1637. foreach($triples as $triple) {
  1638. if($graph_name === '@default') {
  1639. $graph_name = null;
  1640. }
  1641. $quads[] = self::toNQuad($triple, $graph_name);
  1642. }
  1643. }
  1644. sort($quads);
  1645. return implode($quads);
  1646. }
  1647. /**
  1648. * Converts an RDF triple and graph name to an N-Quad string (a single quad).
  1649. *
  1650. * @param stdClass $triple the RDF triple to convert.
  1651. * @param mixed $graph_name the name of the graph containing the triple, null
  1652. * for the default graph.
  1653. * @param string $bnode the bnode the quad is mapped to (optional, for
  1654. * use during normalization only).
  1655. *
  1656. * @return string the N-Quad string.
  1657. */
  1658. public static function toNQuad($triple, $graph_name, $bnode=null) {
  1659. $s = $triple->subject;
  1660. $p = $triple->predicate;
  1661. $o = $triple->object;
  1662. $g = $graph_name;
  1663. $quad = '';
  1664. // subject is an IRI
  1665. if($s->type === 'IRI') {
  1666. $quad .= "<{$s->value}>";
  1667. } else if($bnode !== null) {
  1668. // bnode normalization mode
  1669. $quad .= ($s->value === $bnode) ? '_:a' : '_:z';
  1670. } else {
  1671. // bnode normal mode
  1672. $quad .= $s->value;
  1673. }
  1674. $quad .= ' ';
  1675. // predicate is an IRI
  1676. if($p->type === 'IRI') {
  1677. $quad .= "<{$p->value}>";
  1678. } else if($bnode !== null) {
  1679. // FIXME: TBD what to do with bnode predicates during normalization
  1680. // bnode normalization mode
  1681. $quad .= '_:p';
  1682. } else {
  1683. // bnode normal mode
  1684. $quad .= $p->value;
  1685. }
  1686. $quad .= ' ';
  1687. // object is IRI, bnode, or literal
  1688. if($o->type === 'IRI') {
  1689. $quad .= "<{$o->value}>";
  1690. } else if($o->type === 'blank node') {
  1691. if($bnode !== null) {
  1692. // normalization mode
  1693. $quad .= ($o->value === $bnode) ? '_:a' : '_:z';
  1694. } else {
  1695. // normal mode
  1696. $quad .= $o->value;
  1697. }
  1698. } else {
  1699. $escaped = str_replace(
  1700. array('\\', "\t", "\n", "\r", '"'),
  1701. array('\\\\', '\t', '\n', '\r', '\"'),
  1702. $o->value);
  1703. $quad .= '"' . $escaped . '"';
  1704. if($o->datatype === self::RDF_LANGSTRING) {
  1705. if($o->language) {
  1706. $quad .= "@{$o->language}";
  1707. }
  1708. } else if($o->datatype !== self::XSD_STRING) {
  1709. $quad .= "^^<{$o->datatype}>";
  1710. }
  1711. }
  1712. // graph
  1713. if($g !== null) {
  1714. if(strpos($g, '_:') !== 0) {
  1715. $quad .= " <$g>";
  1716. } else if($bnode) {
  1717. $quad .= ' _:g';
  1718. } else {
  1719. $quad .= " $g";
  1720. }
  1721. }
  1722. $quad .= " .\n";
  1723. return $quad;
  1724. }
  1725. /**
  1726. * Registers a processor-specific RDF dataset parser by content-type.
  1727. * Global parsers will no longer be used by this processor.
  1728. *
  1729. * @param string $content_type the content-type for the parser.
  1730. * @param callable $parser(input) the parser function (takes a string as
  1731. * a parameter and returns an RDF dataset).
  1732. */
  1733. public function registerRDFParser($content_type, $parser) {
  1734. if($this->rdfParsers === null) {
  1735. $this->rdfParsers = new stdClass();
  1736. }
  1737. $this->rdfParsers->{$content_type} = $parser;
  1738. }
  1739. /**
  1740. * Unregisters a process-specific RDF dataset parser by content-type. If
  1741. * there are no remaining processor-specific parsers, then the global
  1742. * parsers will be re-enabled.
  1743. *
  1744. * @param string $content_type the content-type for the parser.
  1745. */
  1746. public function unregisterRDFParser($content_type) {
  1747. if($this->rdfParsers !== null &&
  1748. property_exists($this->rdfParsers, $content_type)) {
  1749. unset($this->rdfParsers->{$content_type});
  1750. if(count(get_object_vars($content_type)) === 0) {
  1751. $this->rdfParsers = null;
  1752. }
  1753. }
  1754. }
  1755. /**
  1756. * If $value is an array, returns $value, otherwise returns an array
  1757. * containing $value as the only element.
  1758. *
  1759. * @param mixed $value the value.
  1760. *
  1761. * @return array an array.
  1762. */
  1763. public static function arrayify($value) {
  1764. return is_array($value) ? $value : array($value);
  1765. }
  1766. /**
  1767. * Clones an object, array, or string/number.
  1768. *
  1769. * @param mixed $value the value to clone.
  1770. *
  1771. * @return mixed the cloned value.
  1772. */
  1773. public static function copy($value) {
  1774. if(is_object($value) || is_array($value)) {
  1775. return unserialize(serialize($value));
  1776. }
  1777. return $value;
  1778. }
  1779. /**
  1780. * Sets the value of a key for the given array if that property
  1781. * has not already been set.
  1782. *
  1783. * @param &assoc $arr the object to update.
  1784. * @param string $key the key to update.
  1785. * @param mixed $value the value to set.
  1786. */
  1787. public static function setdefault(&$arr, $key, $value) {
  1788. isset($arr[$key]) or $arr[$key] = $value;
  1789. }
  1790. /**
  1791. * Sets default values for keys in the given array.
  1792. *
  1793. * @param &assoc $arr the object to update.
  1794. * @param assoc $defaults the default keys and values.
  1795. */
  1796. public static function setdefaults(&$arr, $defaults) {
  1797. foreach($defaults as $key => $value) {
  1798. self::setdefault($arr, $key, $value);
  1799. }
  1800. }
  1801. /**
  1802. * Recursively compacts an element using the given active context. All values
  1803. * must be in expanded form before this method is called.
  1804. *
  1805. * @param stdClass $active_ctx the active context to use.
  1806. * @param mixed $active_property the compacted property with the element
  1807. * to compact, null for none.
  1808. * @param mixed $element the element to compact.
  1809. * @param assoc $options the compaction options.
  1810. *
  1811. * @return mixed the compacted value.
  1812. */
  1813. protected function _compact(
  1814. $active_ctx, $active_property, $element, $options) {
  1815. // recursively compact array
  1816. if(is_array($element)) {
  1817. $rval = array();
  1818. foreach($element as $e) {
  1819. // compact, dropping any null values
  1820. $compacted = $this->_compact(
  1821. $active_ctx, $active_property, $e, $options);
  1822. if($compacted !== null) {
  1823. $rval[] = $compacted;
  1824. }
  1825. }
  1826. if($options['compactArrays'] && count($rval) === 1) {
  1827. // use single element if no container is specified
  1828. $container = self::getContextValue(
  1829. $active_ctx, $active_property, '@container');
  1830. if($container === null) {
  1831. $rval = $rval[0];
  1832. }
  1833. }
  1834. return $rval;
  1835. }
  1836. // recursively compact object
  1837. if(is_object($element)) {
  1838. if($options['link'] && property_exists($element, '@id') &&
  1839. isset($options['link'][$element->{'@id'}])) {
  1840. // check for a linked element to reuse
  1841. $linked = $options['link'][$element->{'@id'}];
  1842. foreach($linked as $link) {
  1843. if($link['expanded'] === $element) {
  1844. return $link['compacted'];
  1845. }
  1846. }
  1847. }
  1848. // do value compaction on @values and subject references
  1849. if(self::_isValue($element) || self::_isSubjectReference($element)) {
  1850. $rval = $this->_compactValue($active_ctx, $active_property, $element);
  1851. if($options['link'] && self::_isSubjectReference($element)) {
  1852. // store linked element
  1853. if(!isset($options['link'][$element->{'@id'}])) {
  1854. $options['link'][$element->{'@id'}] = array();
  1855. }
  1856. $options['link'][$element->{'@id'}][] = array(
  1857. 'expanded' => $element, 'compacted' => $rval);
  1858. }
  1859. return $rval;
  1860. }
  1861. // FIXME: avoid misuse of active property as an expanded property?
  1862. $inside_reverse = ($active_property === '@reverse');
  1863. $rval = new stdClass();
  1864. if($options['link'] && property_exists($element, '@id')) {
  1865. // store linked element
  1866. if(!isset($options['link'][$element->{'@id'}])) {
  1867. $options['link'][$element->{'@id'}] = array();
  1868. }
  1869. $options['link'][$element->{'@id'}][] = array(
  1870. 'expanded' => $element, 'compacted' => $rval);
  1871. }
  1872. // process element keys in order
  1873. $keys = array_keys((array)$element);
  1874. sort($keys);
  1875. foreach($keys as $expanded_property) {
  1876. $expanded_value = $element->{$expanded_property};
  1877. // compact @id and @type(s)
  1878. if($expanded_property === '@id' || $expanded_property === '@type') {
  1879. if(is_string($expanded_value)) {
  1880. // compact single @id
  1881. $compacted_value = $this->_compactIri(
  1882. $active_ctx, $expanded_value, null,
  1883. array('vocab' => ($expanded_property === '@type')));
  1884. } else {
  1885. // expanded value must be a @type array
  1886. $compacted_value = array();
  1887. foreach($expanded_value as $ev) {
  1888. $compacted_value[] = $this->_compactIri(
  1889. $active_ctx, $ev, null, array('vocab' => true));
  1890. }
  1891. }
  1892. // use keyword alias and add value
  1893. $alias = $this->_compactIri($active_ctx, $expanded_property);
  1894. $is_array = (is_array($compacted_value) &&
  1895. count($expanded_value) === 0);
  1896. self::addValue(
  1897. $rval, $alias, $compacted_value,
  1898. array('propertyIsArray' => $is_array));
  1899. continue;
  1900. }
  1901. // handle @reverse
  1902. if($expanded_property === '@reverse') {
  1903. // recursively compact expanded value
  1904. $compacted_value = $this->_compact(
  1905. $active_ctx, '@reverse', $expanded_value, $options);
  1906. // handle double-reversed properties
  1907. foreach($compacted_value as $compacted_property => $value) {
  1908. if(property_exists($active_ctx->mappings, $compacted_property) &&
  1909. $active_ctx->mappings->{$compacted_property} &&
  1910. $active_ctx->mappings->{$compacted_property}->reverse) {
  1911. $container = self::getContextValue(
  1912. $active_ctx, $compacted_property, '@container');
  1913. $use_array = ($container === '@set' ||
  1914. !$options['compactArrays']);
  1915. self::addValue(
  1916. $rval, $compacted_property, $value,
  1917. array('propertyIsArray' => $use_array));
  1918. unset($compacted_value->{$compacted_property});
  1919. }
  1920. }
  1921. if(count(array_keys((array)$compacted_value)) > 0) {
  1922. // use keyword alias and add value
  1923. $alias = $this->_compactIri($active_ctx, $expanded_property);
  1924. self::addValue($rval, $alias, $compacted_value);
  1925. }
  1926. continue;
  1927. }
  1928. // handle @index property
  1929. if($expanded_property === '@index') {
  1930. // drop @index if inside an @index container
  1931. $container = self::getContextValue(
  1932. $active_ctx, $active_property, '@container');
  1933. if($container === '@index') {
  1934. continue;
  1935. }
  1936. // use keyword alias and add value
  1937. $alias = $this->_compactIri($active_ctx, $expanded_property);
  1938. self::addValue($rval, $alias, $expanded_value);
  1939. continue;
  1940. }
  1941. // skip array processing for keywords that aren't @graph or @list
  1942. if($expanded_property !== '@graph' && $expanded_property !== '@list' &&
  1943. self::_isKeyword($expanded_property)) {
  1944. // use keyword alias and add value as is
  1945. $alias = $this->_compactIri($active_ctx, $expanded_property);
  1946. self::addValue($rval, $alias, $expanded_value);
  1947. continue;
  1948. }
  1949. // Note: expanded value must be an array due to expansion algorithm.
  1950. // preserve empty arrays
  1951. if(count($expanded_value) === 0) {
  1952. $item_active_property = $this->_compactIri(
  1953. $active_ctx, $expanded_property, $expanded_value,
  1954. array('vocab' => true), $inside_reverse);
  1955. self::addValue(
  1956. $rval, $item_active_property, array(),
  1957. array('propertyIsArray' => true));
  1958. }
  1959. // recusively process array values
  1960. foreach($expanded_value as $expanded_item) {
  1961. // compact property and get container type
  1962. $item_active_property = $this->_compactIri(
  1963. $active_ctx, $expanded_property, $expanded_item,
  1964. array('vocab' => true), $inside_reverse);
  1965. $container = self::getContextValue(
  1966. $active_ctx, $item_active_property, '@container');
  1967. // get @list value if appropriate
  1968. $is_list = self::_isList($expanded_item);
  1969. $list = null;
  1970. if($is_list) {
  1971. $list = $expanded_item->{'@list'};
  1972. }
  1973. // recursively compact expanded item
  1974. $compacted_item = $this->_compact(
  1975. $active_ctx, $item_active_property,
  1976. $is_list ? $list : $expanded_item, $options);
  1977. // handle @list
  1978. if($is_list) {
  1979. // ensure @list value is an array
  1980. $compacted_item = self::arrayify($compacted_item);
  1981. if($container !== '@list') {
  1982. // wrap using @list alias
  1983. $compacted_item = (object)array(
  1984. $this->_compactIri($active_ctx, '@list') => $compacted_item);
  1985. // include @index from expanded @list, if any
  1986. if(property_exists($expanded_item, '@index')) {
  1987. $compacted_item->{$this->_compactIri($active_ctx, '@index')} =
  1988. $expanded_item->{'@index'};
  1989. }
  1990. } else if(property_exists($rval, $item_active_property)) {
  1991. // can't use @list container for more than 1 list
  1992. throw new JsonLdException(
  1993. 'JSON-LD compact error; property has a "@list" @container ' .
  1994. 'rule but there is more than a single @list that matches ' .
  1995. 'the compacted term in the document. Compaction might mix ' .
  1996. 'unwanted items into the list.', 'jsonld.SyntaxError',
  1997. 'compaction to list of lists');
  1998. }
  1999. }
  2000. // handle language and index maps
  2001. if($container === '@language' || $container === '@index') {
  2002. // get or create the map object
  2003. if(property_exists($rval, $item_active_property)) {
  2004. $map_object = $rval->{$item_active_property};
  2005. } else {
  2006. $rval->{$item_active_property} = $map_object = new stdClass();
  2007. }
  2008. // if container is a language map, simplify compacted value to
  2009. // a simple string
  2010. if($container === '@language' && self::_isValue($compacted_item)) {
  2011. $compacted_item = $compacted_item->{'@value'};
  2012. }
  2013. // add compact value to map object using key from expanded value
  2014. // based on the container type
  2015. self::addValue(
  2016. $map_object, $expanded_item->{$container}, $compacted_item);
  2017. } else {
  2018. // use an array if: compactArrays flag is false,
  2019. // @container is @set or @list, value is an empty
  2020. // array, or key is @graph
  2021. $is_array = (!$options['compactArrays'] ||
  2022. $container === '@set' || $container === '@list' ||
  2023. (is_array($compacted_item) && count($compacted_item) === 0) ||
  2024. $expanded_property === '@list' ||
  2025. $expanded_property === '@graph');
  2026. // add compact value
  2027. self::addValue(
  2028. $rval, $item_active_property, $compacted_item,
  2029. array('propertyIsArray' => $is_array));
  2030. }
  2031. }
  2032. }
  2033. return $rval;
  2034. }
  2035. // only primitives remain which are already compact
  2036. return $element;
  2037. }
  2038. /**
  2039. * Recursively expands an element using the given context. Any context in
  2040. * the element will be removed. All context URLs must have been retrieved
  2041. * before calling this method.
  2042. *
  2043. * @param stdClass $active_ctx the active context to use.
  2044. * @param mixed $active_property the property for the element, null for none.
  2045. * @param mixed $element the element to expand.
  2046. * @param assoc $options the expansion options.
  2047. * @param bool $inside_list true if the property is a list, false if not.
  2048. *
  2049. * @return mixed the expanded value.
  2050. */
  2051. protected function _expand(
  2052. $active_ctx, $active_property, $element, $options, $inside_list) {
  2053. // nothing to expand
  2054. if($element === null) {
  2055. return $element;
  2056. }
  2057. // recursively expand array
  2058. if(is_array($element)) {
  2059. $rval = array();
  2060. $container = self::getContextValue(
  2061. $active_ctx, $active_property, '@container');
  2062. $inside_list = $inside_list || $container === '@list';
  2063. foreach($element as $e) {
  2064. // expand element
  2065. $e = $this->_expand(
  2066. $active_ctx, $active_property, $e, $options, $inside_list);
  2067. if($inside_list && (is_array($e) || self::_isList($e))) {
  2068. // lists of lists are illegal
  2069. throw new JsonLdException(
  2070. 'Invalid JSON-LD syntax; lists of lists are not permitted.',
  2071. 'jsonld.SyntaxError', 'list of lists');
  2072. }
  2073. // drop null values
  2074. if($e !== null) {
  2075. if(is_array($e)) {
  2076. $rval = array_merge($rval, $e);
  2077. } else {
  2078. $rval[] = $e;
  2079. }
  2080. }
  2081. }
  2082. return $rval;
  2083. }
  2084. if(!is_object($element)) {
  2085. // drop free-floating scalars that are not in lists
  2086. if(!$inside_list &&
  2087. ($active_property === null ||
  2088. $this->_expandIri($active_ctx, $active_property,
  2089. array('vocab' => true)) === '@graph')) {
  2090. return null;
  2091. }
  2092. // expand element according to value expansion rules
  2093. return $this->_expandValue($active_ctx, $active_property, $element);
  2094. }
  2095. // recursively expand object:
  2096. // if element has a context, process it
  2097. if(property_exists($element, '@context')) {
  2098. $active_ctx = $this->_processContext(
  2099. $active_ctx, $element->{'@context'}, $options);
  2100. }
  2101. // expand the active property
  2102. $expanded_active_property = $this->_expandIri(
  2103. $active_ctx, $active_property, array('vocab' => true));
  2104. $rval = new stdClass();
  2105. $keys = array_keys((array)$element);
  2106. sort($keys);
  2107. foreach($keys as $key) {
  2108. $value = $element->{$key};
  2109. if($key === '@context') {
  2110. continue;
  2111. }
  2112. // expand key to IRI
  2113. $expanded_property = $this->_expandIri(
  2114. $active_ctx, $key, array('vocab' => true));
  2115. // drop non-absolute IRI keys that aren't keywords
  2116. if($expanded_property === null ||
  2117. !(self::_isAbsoluteIri($expanded_property) ||
  2118. self::_isKeyword($expanded_property))) {
  2119. continue;
  2120. }
  2121. if(self::_isKeyword($expanded_property)) {
  2122. if($expanded_active_property === '@reverse') {
  2123. throw new JsonLdException(
  2124. 'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' .
  2125. 'property.', 'jsonld.SyntaxError', 'invalid reverse property map',
  2126. array('value' => $value));
  2127. }
  2128. if(property_exists($rval, $expanded_property)) {
  2129. throw new JsonLdException(
  2130. 'Invalid JSON-LD syntax; colliding keywords detected.',
  2131. 'jsonld.SyntaxError', 'colliding keywords',
  2132. array('keyword' => $expanded_property));
  2133. }
  2134. }
  2135. // syntax error if @id is not a string
  2136. if($expanded_property === '@id' && !is_string($value)) {
  2137. if(!isset($options['isFrame']) || !$options['isFrame']) {
  2138. throw new JsonLdException(
  2139. 'Invalid JSON-LD syntax; "@id" value must a string.',
  2140. 'jsonld.SyntaxError', 'invalid @id value',
  2141. array('value' => $value));
  2142. }
  2143. if(!is_object($value)) {
  2144. throw new JsonLdException(
  2145. 'Invalid JSON-LD syntax; "@id" value must a string or an object.',
  2146. 'jsonld.SyntaxError', 'invalid @id value',
  2147. array('value' => $value));
  2148. }
  2149. }
  2150. // validate @type value
  2151. if($expanded_property === '@type') {
  2152. $this->_validateTypeValue($value);
  2153. }
  2154. // @graph must be an array or an object
  2155. if($expanded_property === '@graph' &&
  2156. !(is_object($value) || is_array($value))) {
  2157. throw new JsonLdException(
  2158. 'Invalid JSON-LD syntax; "@graph" value must not be an ' .
  2159. 'object or an array.', 'jsonld.SyntaxError',
  2160. 'invalid @graph value', array('value' => $value));
  2161. }
  2162. // @value must not be an object or an array
  2163. if($expanded_property === '@value' &&
  2164. (is_object($value) || is_array($value))) {
  2165. throw new JsonLdException(
  2166. 'Invalid JSON-LD syntax; "@value" value must not be an ' .
  2167. 'object or an array.', 'jsonld.SyntaxError',
  2168. 'invalid value object value', array('value' => $value));
  2169. }
  2170. // @language must be a string
  2171. if($expanded_property === '@language') {
  2172. if($value === null) {
  2173. // drop null @language values, they expand as if they didn't exist
  2174. continue;
  2175. }
  2176. if(!is_string($value)) {
  2177. throw new JsonLdException(
  2178. 'Invalid JSON-LD syntax; "@language" value must not be a string.',
  2179. 'jsonld.SyntaxError', 'invalid language-tagged string',
  2180. array('value' => $value));
  2181. }
  2182. // ensure language value is lowercase
  2183. $value = strtolower($value);
  2184. }
  2185. // @index must be a string
  2186. if($expanded_property === '@index') {
  2187. if(!is_string($value)) {
  2188. throw new JsonLdException(
  2189. 'Invalid JSON-LD syntax; "@index" value must be a string.',
  2190. 'jsonld.SyntaxError', 'invalid @index value',
  2191. array('value' => $value));
  2192. }
  2193. }
  2194. // @reverse must be an object
  2195. if($expanded_property === '@reverse') {
  2196. if(!is_object($value)) {
  2197. throw new JsonLdException(
  2198. 'Invalid JSON-LD syntax; "@reverse" value must be an object.',
  2199. 'jsonld.SyntaxError', 'invalid @reverse value',
  2200. array('value' => $value));
  2201. }
  2202. $expanded_value = $this->_expand(
  2203. $active_ctx, '@reverse', $value, $options, $inside_list);
  2204. // properties double-reversed
  2205. if(property_exists($expanded_value, '@reverse')) {
  2206. foreach($expanded_value->{'@reverse'} as $rproperty => $rvalue) {
  2207. self::addValue(
  2208. $rval, $rproperty, $rvalue, array('propertyIsArray' => true));
  2209. }
  2210. }
  2211. // FIXME: can this be merged with code below to simplify?
  2212. // merge in all reversed properties
  2213. if(property_exists($rval, '@reverse')) {
  2214. $reverse_map = $rval->{'@reverse'};
  2215. } else {
  2216. $reverse_map = null;
  2217. }
  2218. foreach($expanded_value as $property => $items) {
  2219. if($property === '@reverse') {
  2220. continue;
  2221. }
  2222. if($reverse_map === null) {
  2223. $reverse_map = $rval->{'@reverse'} = new stdClass();
  2224. }
  2225. self::addValue(
  2226. $reverse_map, $property, array(),
  2227. array('propertyIsArray' => true));
  2228. foreach($items as $item) {
  2229. if(self::_isValue($item) || self::_isList($item)) {
  2230. throw new JsonLdException(
  2231. 'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
  2232. '@value or an @list.', 'jsonld.SyntaxError',
  2233. 'invalid reverse property value',
  2234. array('value' => $expanded_value));
  2235. }
  2236. self::addValue(
  2237. $reverse_map, $property, $item,
  2238. array('propertyIsArray' => true));
  2239. }
  2240. }
  2241. continue;
  2242. }
  2243. $container = self::getContextValue($active_ctx, $key, '@container');
  2244. if($container === '@language' && is_object($value)) {
  2245. // handle language map container (skip if value is not an object)
  2246. $expanded_value = $this->_expandLanguageMap($value);
  2247. } else if($container === '@index' && is_object($value)) {
  2248. // handle index container (skip if value is not an object)
  2249. $expanded_value = array();
  2250. $value_keys = array_keys((array)$value);
  2251. sort($value_keys);
  2252. foreach($value_keys as $value_key) {
  2253. $val = $value->{$value_key};
  2254. $val = self::arrayify($val);
  2255. $val = $this->_expand($active_ctx, $key, $val, $options, false);
  2256. foreach($val as $item) {
  2257. if(!property_exists($item, '@index')) {
  2258. $item->{'@index'} = $value_key;
  2259. }
  2260. $expanded_value[] = $item;
  2261. }
  2262. }
  2263. } else {
  2264. // recurse into @list or @set
  2265. $is_list = ($expanded_property === '@list');
  2266. if($is_list || $expanded_property === '@set') {
  2267. $next_active_property = $active_property;
  2268. if($is_list && $expanded_active_property === '@graph') {
  2269. $next_active_property = null;
  2270. }
  2271. $expanded_value = $this->_expand(
  2272. $active_ctx, $next_active_property, $value, $options, $is_list);
  2273. if($is_list && self::_isList($expanded_value)) {
  2274. throw new JsonLdException(
  2275. 'Invalid JSON-LD syntax; lists of lists are not permitted.',
  2276. 'jsonld.SyntaxError', 'list of lists');
  2277. }
  2278. } else {
  2279. // recursively expand value with key as new active property
  2280. $expanded_value = $this->_expand(
  2281. $active_ctx, $key, $value, $options, false);
  2282. }
  2283. }
  2284. // drop null values if property is not @value
  2285. if($expanded_value === null && $expanded_property !== '@value') {
  2286. continue;
  2287. }
  2288. // convert expanded value to @list if container specifies it
  2289. if($expanded_property !== '@list' && !self::_isList($expanded_value) &&
  2290. $container === '@list') {
  2291. // ensure expanded value is an array
  2292. $expanded_value = (object)array(
  2293. '@list' => self::arrayify($expanded_value));
  2294. }
  2295. // FIXME: can this be merged with code above to simplify?
  2296. // merge in reverse properties
  2297. if(property_exists($active_ctx->mappings, $key) &&
  2298. $active_ctx->mappings->{$key} &&
  2299. $active_ctx->mappings->{$key}->reverse) {
  2300. if(property_exists($rval, '@reverse')) {
  2301. $reverse_map = $rval->{'@reverse'};
  2302. } else {
  2303. $reverse_map = $rval->{'@reverse'} = new stdClass();
  2304. }
  2305. $expanded_value = self::arrayify($expanded_value);
  2306. foreach($expanded_value as $item) {
  2307. if(self::_isValue($item) || self::_isList($item)) {
  2308. throw new JsonLdException(
  2309. 'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
  2310. '@value or an @list.', 'jsonld.SyntaxError',
  2311. 'invalid reverse property value',
  2312. array('value' => $expanded_value));
  2313. }
  2314. self::addValue(
  2315. $reverse_map, $expanded_property, $item,
  2316. array('propertyIsArray' => true));
  2317. }
  2318. continue;
  2319. }
  2320. // add value for property
  2321. // use an array except for certain keywords
  2322. $use_array = (!in_array(
  2323. $expanded_property, array(
  2324. '@index', '@id', '@type', '@value', '@language')));
  2325. self::addValue(
  2326. $rval, $expanded_property, $expanded_value,
  2327. array('propertyIsArray' => $use_array));
  2328. }
  2329. // get property count on expanded output
  2330. $keys = array_keys((array)$rval);
  2331. $count = count($keys);
  2332. // @value must only have @language or @type
  2333. if(property_exists($rval, '@value')) {
  2334. // @value must only have @language or @type
  2335. if(property_exists($rval, '@type') &&
  2336. property_exists($rval, '@language')) {
  2337. throw new JsonLdException(
  2338. 'Invalid JSON-LD syntax; an element containing "@value" may not ' .
  2339. 'contain both "@type" and "@language".',
  2340. 'jsonld.SyntaxError', 'invalid value object',
  2341. array('element' => $rval));
  2342. }
  2343. $valid_count = $count - 1;
  2344. if(property_exists($rval, '@type')) {
  2345. $valid_count -= 1;
  2346. }
  2347. if(property_exists($rval, '@index')) {
  2348. $valid_count -= 1;
  2349. }
  2350. if(property_exists($rval, '@language')) {
  2351. $valid_count -= 1;
  2352. }
  2353. if($valid_count !== 0) {
  2354. throw new JsonLdException(
  2355. 'Invalid JSON-LD syntax; an element containing "@value" may only ' .
  2356. 'have an "@index" property and at most one other property ' .
  2357. 'which can be "@type" or "@language".',
  2358. 'jsonld.SyntaxError', 'invalid value object',
  2359. array('element' => $rval));
  2360. }
  2361. // drop null @values
  2362. if($rval->{'@value'} === null) {
  2363. $rval = null;
  2364. } else if(property_exists($rval, '@language') &&
  2365. !is_string($rval->{'@value'})) {
  2366. // if @language is present, @value must be a string
  2367. throw new JsonLdException(
  2368. 'Invalid JSON-LD syntax; only strings may be language-tagged.',
  2369. 'jsonld.SyntaxError', 'invalid language-tagged value',
  2370. array('element' => $rval));
  2371. } else if(property_exists($rval, '@type') &&
  2372. (!self::_isAbsoluteIri($rval->{'@type'}) ||
  2373. strpos($rval->{'@type'}, '_:') === 0)) {
  2374. throw new JsonLdException(
  2375. 'Invalid JSON-LD syntax; an element containing "@value" ' .
  2376. 'and "@type" must have an absolute IRI for the value ' .
  2377. 'of "@type".', 'jsonld.SyntaxError', 'invalid typed value',
  2378. array('element' => $rval));
  2379. }
  2380. } else if(property_exists($rval, '@type') && !is_array($rval->{'@type'})) {
  2381. // convert @type to an array
  2382. $rval->{'@type'} = array($rval->{'@type'});
  2383. } else if(property_exists($rval, '@set') ||
  2384. property_exists($rval, '@list')) {
  2385. // handle @set and @list
  2386. if($count > 1 && !($count === 2 && property_exists($rval, '@index'))) {
  2387. throw new JsonLdException(
  2388. 'Invalid JSON-LD syntax; if an element has the property "@set" ' .
  2389. 'or "@list", then it can have at most one other property that is ' .
  2390. '"@index".', 'jsonld.SyntaxError', 'invalid set or list object',
  2391. array('element' => $rval));
  2392. }
  2393. // optimize away @set
  2394. if(property_exists($rval, '@set')) {
  2395. $rval = $rval->{'@set'};
  2396. $keys = array_keys((array)$rval);
  2397. $count = count($keys);
  2398. }
  2399. } else if($count === 1 && property_exists($rval, '@language')) {
  2400. // drop objects with only @language
  2401. $rval = null;
  2402. }
  2403. // drop certain top-level objects that do not occur in lists
  2404. if(is_object($rval) &&
  2405. !$options['keepFreeFloatingNodes'] && !$inside_list &&
  2406. ($active_property === null || $expanded_active_property === '@graph')) {
  2407. // drop empty object or top-level @value/@list, or object with only @id
  2408. if($count === 0 || property_exists($rval, '@value') ||
  2409. property_exists($rval, '@list') ||
  2410. ($count === 1 && property_exists($rval, '@id'))) {
  2411. $rval = null;
  2412. }
  2413. }
  2414. return $rval;
  2415. }
  2416. /**
  2417. * Performs JSON-LD flattening.
  2418. *
  2419. * @param array $input the expanded JSON-LD to flatten.
  2420. *
  2421. * @return array the flattened output.
  2422. */
  2423. protected function _flatten($input) {
  2424. // produce a map of all subjects and name each bnode
  2425. $namer = new UniqueNamer('_:b');
  2426. $graphs = (object)array('@default' => new stdClass());
  2427. $this->_createNodeMap($input, $graphs, '@default', $namer);
  2428. // add all non-default graphs to default graph
  2429. $default_graph = $graphs->{'@default'};
  2430. $graph_names = array_keys((array)$graphs);
  2431. foreach($graph_names as $graph_name) {
  2432. if($graph_name === '@default') {
  2433. continue;
  2434. }
  2435. $node_map = $graphs->{$graph_name};
  2436. if(!property_exists($default_graph, $graph_name)) {
  2437. $default_graph->{$graph_name} = (object)array(
  2438. '@id' => $graph_name, '@graph' => array());
  2439. }
  2440. $subject = $default_graph->{$graph_name};
  2441. if(!property_exists($subject, '@graph')) {
  2442. $subject->{'@graph'} = array();
  2443. }
  2444. $ids = array_keys((array)$node_map);
  2445. sort($ids);
  2446. foreach($ids as $id) {
  2447. $node = $node_map->{$id};
  2448. // only add full subjects
  2449. if(!self::_isSubjectReference($node)) {
  2450. $subject->{'@graph'}[] = $node;
  2451. }
  2452. }
  2453. }
  2454. // produce flattened output
  2455. $flattened = array();
  2456. $keys = array_keys((array)$default_graph);
  2457. sort($keys);
  2458. foreach($keys as $key) {
  2459. $node = $default_graph->{$key};
  2460. // only add full subjects to top-level
  2461. if(!self::_isSubjectReference($node)) {
  2462. $flattened[] = $node;
  2463. }
  2464. }
  2465. return $flattened;
  2466. }
  2467. /**
  2468. * Performs JSON-LD framing.
  2469. *
  2470. * @param array $input the expanded JSON-LD to frame.
  2471. * @param array $frame the expanded JSON-LD frame to use.
  2472. * @param assoc $options the framing options.
  2473. *
  2474. * @return array the framed output.
  2475. */
  2476. protected function _frame($input, $frame, $options) {
  2477. // create framing state
  2478. $state = (object)array(
  2479. 'options' => $options,
  2480. 'graphs' => (object)array(
  2481. '@default' => new stdClass(),
  2482. '@merged' => new stdClass()),
  2483. 'subjectStack' => array(),
  2484. 'link' => new stdClass());
  2485. // produce a map of all graphs and name each bnode
  2486. // FIXME: currently uses subjects from @merged graph only
  2487. $namer = new UniqueNamer('_:b');
  2488. $this->_createNodeMap($input, $state->graphs, '@merged', $namer);
  2489. $state->subjects = $state->graphs->{'@merged'};
  2490. // frame the subjects
  2491. $framed = new ArrayObject();
  2492. $keys = array_keys((array)$state->subjects);
  2493. sort($keys);
  2494. $this->_matchFrame($state, $keys, $frame, $framed, null);
  2495. return (array)$framed;
  2496. }
  2497. /**
  2498. * Performs normalization on the given RDF dataset.
  2499. *
  2500. * @param stdClass $dataset the RDF dataset to normalize.
  2501. * @param assoc $options the normalization options.
  2502. *
  2503. * @return mixed the normalized output.
  2504. */
  2505. protected function _normalize($dataset, $options) {
  2506. // create quads and map bnodes to their associated quads
  2507. $quads = array();
  2508. $bnodes = new stdClass();
  2509. foreach($dataset as $graph_name => $triples) {
  2510. if($graph_name === '@default') {
  2511. $graph_name = null;
  2512. }
  2513. foreach($triples as $triple) {
  2514. $quad = $triple;
  2515. if($graph_name !== null) {
  2516. if(strpos($graph_name, '_:') === 0) {
  2517. $quad->name = (object)array(
  2518. 'type' => 'blank node', 'value' => $graph_name);
  2519. } else {
  2520. $quad->name = (object)array(
  2521. 'type' => 'IRI', 'value' => $graph_name);
  2522. }
  2523. }
  2524. $quads[] = $quad;
  2525. foreach(array('subject', 'object', 'name') as $attr) {
  2526. if(property_exists($quad, $attr) &&
  2527. $quad->{$attr}->type === 'blank node') {
  2528. $id = $quad->{$attr}->value;
  2529. if(property_exists($bnodes, $id)) {
  2530. $bnodes->{$id}->quads[] = $quad;
  2531. } else {
  2532. $bnodes->{$id} = (object)array('quads' => array($quad));
  2533. }
  2534. }
  2535. }
  2536. }
  2537. }
  2538. // mapping complete, start canonical naming
  2539. $namer = new UniqueNamer('_:c14n');
  2540. // continue to hash bnode quads while bnodes are assigned names
  2541. $unnamed = null;
  2542. $nextUnnamed = array_keys((array)$bnodes);
  2543. $duplicates = null;
  2544. do {
  2545. $unnamed = $nextUnnamed;
  2546. $nextUnnamed = array();
  2547. $duplicates = new stdClass();
  2548. $unique = new stdClass();
  2549. foreach($unnamed as $bnode) {
  2550. // hash quads for each unnamed bnode
  2551. $hash = $this->_hashQuads($bnode, $bnodes, $namer);
  2552. // store hash as unique or a duplicate
  2553. if(property_exists($duplicates, $hash)) {
  2554. $duplicates->{$hash}[] = $bnode;
  2555. $nextUnnamed[] = $bnode;
  2556. } else if(property_exists($unique, $hash)) {
  2557. $duplicates->{$hash} = array($unique->{$hash}, $bnode);
  2558. $nextUnnamed[] = $unique->{$hash};
  2559. $nextUnnamed[] = $bnode;
  2560. unset($unique->{$hash});
  2561. } else {
  2562. $unique->{$hash} = $bnode;
  2563. }
  2564. }
  2565. // name unique bnodes in sorted hash order
  2566. $hashes = array_keys((array)$unique);
  2567. sort($hashes);
  2568. foreach($hashes as $hash) {
  2569. $namer->getName($unique->{$hash});
  2570. }
  2571. }
  2572. while(count($unnamed) > count($nextUnnamed));
  2573. // enumerate duplicate hash groups in sorted order
  2574. $hashes = array_keys((array)$duplicates);
  2575. sort($hashes);
  2576. foreach($hashes as $hash) {
  2577. // process group
  2578. $group = $duplicates->{$hash};
  2579. $results = array();
  2580. foreach($group as $bnode) {
  2581. // skip already-named bnodes
  2582. if($namer->isNamed($bnode)) {
  2583. continue;
  2584. }
  2585. // hash bnode paths
  2586. $path_namer = new UniqueNamer('_:b');
  2587. $path_namer->getName($bnode);
  2588. $results[] = $this->_hashPaths($bnode, $bnodes, $namer, $path_namer);
  2589. }
  2590. // name bnodes in hash order
  2591. usort($results, function($a, $b) {
  2592. $a = $a->hash;
  2593. $b = $b->hash;
  2594. return ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
  2595. });
  2596. foreach($results as $result) {
  2597. // name all bnodes in path namer in key-entry order
  2598. foreach($result->pathNamer->order as $bnode) {
  2599. $namer->getName($bnode);
  2600. }
  2601. }
  2602. }
  2603. // create normalized array
  2604. $normalized = array();
  2605. /* Note: At this point all bnodes in the set of RDF quads have been
  2606. assigned canonical names, which have been stored in the 'namer' object.
  2607. Here each quad is updated by assigning each of its bnodes its new name
  2608. via the 'namer' object. */
  2609. // update bnode names in each quad and serialize
  2610. foreach($quads as $quad) {
  2611. foreach(array('subject', 'object', 'name') as $attr) {
  2612. if(property_exists($quad, $attr) &&
  2613. $quad->{$attr}->type === 'blank node' &&
  2614. strpos($quad->{$attr}->value, '_:c14n') !== 0) {
  2615. $quad->{$attr}->value = $namer->getName($quad->{$attr}->value);
  2616. }
  2617. }
  2618. $normalized[] = $this->toNQuad($quad, property_exists($quad, 'name') ?
  2619. $quad->name->value : null);
  2620. }
  2621. // sort normalized output
  2622. sort($normalized);
  2623. // handle output format
  2624. if(isset($options['format']) && $options['format']) {
  2625. if($options['format'] === 'application/nquads') {
  2626. return implode($normalized);
  2627. }
  2628. throw new JsonLdException(
  2629. 'Unknown output format.',
  2630. 'jsonld.UnknownFormat', null, array('format' => $options['format']));
  2631. }
  2632. // return RDF dataset
  2633. return $this->parseNQuads(implode($normalized));
  2634. }
  2635. /**
  2636. * Converts an RDF dataset to JSON-LD.
  2637. *
  2638. * @param stdClass $dataset the RDF dataset.
  2639. * @param assoc $options the RDF serialization options.
  2640. *
  2641. * @return array the JSON-LD output.
  2642. */
  2643. protected function _fromRDF($dataset, $options) {
  2644. $default_graph = new stdClass();
  2645. $graph_map = (object)array('@default' => $default_graph);
  2646. $referenced_once = (object)array();
  2647. foreach($dataset as $name => $graph) {
  2648. if(!property_exists($graph_map, $name)) {
  2649. $graph_map->{$name} = new stdClass();
  2650. }
  2651. if($name !== '@default' && !property_exists($default_graph, $name)) {
  2652. $default_graph->{$name} = (object)array('@id' => $name);
  2653. }
  2654. $node_map = $graph_map->{$name};
  2655. foreach($graph as $triple) {
  2656. // get subject, predicate, object
  2657. $s = $triple->subject->value;
  2658. $p = $triple->predicate->value;
  2659. $o = $triple->object;
  2660. if(!property_exists($node_map, $s)) {
  2661. $node_map->{$s} = (object)array('@id' => $s);
  2662. }
  2663. $node = $node_map->{$s};
  2664. $object_is_id = ($o->type === 'IRI' || $o->type === 'blank node');
  2665. if($object_is_id && !property_exists($node_map, $o->value)) {
  2666. $node_map->{$o->value} = (object)array('@id' => $o->value);
  2667. }
  2668. if($p === self::RDF_TYPE && !$options['useRdfType'] && $object_is_id) {
  2669. self::addValue(
  2670. $node, '@type', $o->value, array('propertyIsArray' => true));
  2671. continue;
  2672. }
  2673. $value = self::_RDFToObject($o, $options['useNativeTypes']);
  2674. self::addValue($node, $p, $value, array('propertyIsArray' => true));
  2675. // object may be an RDF list/partial list node but we can't know
  2676. // easily until all triples are read
  2677. if($object_is_id) {
  2678. if($o->value === self::RDF_NIL) {
  2679. $object = $node_map->{$o->value};
  2680. if(!property_exists($object, 'usages')) {
  2681. $object->usages = array();
  2682. }
  2683. $object->usages[] = (object)array(
  2684. 'node' => $node,
  2685. 'property' => $p,
  2686. 'value' => $value);
  2687. } else if(property_exists($referenced_once, $o->value)) {
  2688. // object referenced more than once
  2689. $referenced_once->{$o->value} = false;
  2690. } else {
  2691. // track single reference
  2692. $referenced_once->{$o->value} = (object)array(
  2693. 'node' => $node,
  2694. 'property' => $p,
  2695. 'value' => $value);
  2696. }
  2697. }
  2698. }
  2699. }
  2700. // convert linked lists to @list arrays
  2701. foreach($graph_map as $name => $graph_object) {
  2702. // no @lists to be converted, continue
  2703. if(!property_exists($graph_object, self::RDF_NIL)) {
  2704. continue;
  2705. }
  2706. // iterate backwards through each RDF list
  2707. $nil = $graph_object->{self::RDF_NIL};
  2708. foreach($nil->usages as $usage) {
  2709. $node = $usage->node;
  2710. $property = $usage->property;
  2711. $head = $usage->value;
  2712. $list = array();
  2713. $list_nodes = array();
  2714. // ensure node is a well-formed list node; it must:
  2715. // 1. Be referenced only once.
  2716. // 2. Have an array for rdf:first that has 1 item.
  2717. // 3. Have an array for rdf:rest that has 1 item.
  2718. // 4. Have no keys other than: @id, rdf:first, rdf:rest, and,
  2719. // optionally, @type where the value is rdf:List.
  2720. $node_key_count = count(array_keys((array)$node));
  2721. while($property === self::RDF_REST &&
  2722. property_exists($referenced_once, $node->{'@id'}) &&
  2723. is_object($referenced_once->{$node->{'@id'}}) &&
  2724. property_exists($node, self::RDF_FIRST) &&
  2725. property_exists($node, self::RDF_REST) &&
  2726. is_array($node->{self::RDF_FIRST}) &&
  2727. is_array($node->{self::RDF_REST}) &&
  2728. count($node->{self::RDF_FIRST}) === 1 &&
  2729. count($node->{self::RDF_REST}) === 1 &&
  2730. ($node_key_count === 3 || ($node_key_count === 4 &&
  2731. property_exists($node, '@type') && is_array($node->{'@type'}) &&
  2732. count($node->{'@type'}) === 1 &&
  2733. $node->{'@type'}[0] === self::RDF_LIST))) {
  2734. $list[] = $node->{self::RDF_FIRST}[0];
  2735. $list_nodes[] = $node->{'@id'};
  2736. // get next node, moving backwards through list
  2737. $usage = $referenced_once->{$node->{'@id'}};
  2738. $node = $usage->node;
  2739. $property = $usage->property;
  2740. $head = $usage->value;
  2741. $node_key_count = count(array_keys((array)$node));
  2742. // if node is not a blank node, then list head found
  2743. if(strpos($node->{'@id'}, '_:') !== 0) {
  2744. break;
  2745. }
  2746. }
  2747. // list is nested in another list
  2748. if($property === self::RDF_FIRST) {
  2749. // empty list
  2750. if($node->{'@id'} === self::RDF_NIL) {
  2751. // can't convert rdf:nil to a @list object because it would
  2752. // result in a list of lists which isn't supported
  2753. continue;
  2754. }
  2755. // preserve list head
  2756. $head = $graph_object->{$head->{'@id'}}->{self::RDF_REST}[0];
  2757. array_pop($list);
  2758. array_pop($list_nodes);
  2759. }
  2760. // transform list into @list object
  2761. unset($head->{'@id'});
  2762. $head->{'@list'} = array_reverse($list);
  2763. foreach($list_nodes as $list_node) {
  2764. unset($graph_object->{$list_node});
  2765. }
  2766. }
  2767. unset($nil->usages);
  2768. }
  2769. $result = array();
  2770. $subjects = array_keys((array)$default_graph);
  2771. sort($subjects);
  2772. foreach($subjects as $subject) {
  2773. $node = $default_graph->{$subject};
  2774. if(property_exists($graph_map, $subject)) {
  2775. $node->{'@graph'} = array();
  2776. $graph_object = $graph_map->{$subject};
  2777. $subjects_ = array_keys((array)$graph_object);
  2778. sort($subjects_);
  2779. foreach($subjects_ as $subject_) {
  2780. $node_ = $graph_object->{$subject_};
  2781. // only add full subjects to top-level
  2782. if(!self::_isSubjectReference($node_)) {
  2783. $node->{'@graph'}[] = $node_;
  2784. }
  2785. }
  2786. }
  2787. // only add full subjects to top-level
  2788. if(!self::_isSubjectReference($node)) {
  2789. $result[] = $node;
  2790. }
  2791. }
  2792. return $result;
  2793. }
  2794. /**
  2795. * Processes a local context and returns a new active context.
  2796. *
  2797. * @param stdClass $active_ctx the current active context.
  2798. * @param mixed $local_ctx the local context to process.
  2799. * @param assoc $options the context processing options.
  2800. *
  2801. * @return stdClass the new active context.
  2802. */
  2803. protected function _processContext($active_ctx, $local_ctx, $options) {
  2804. global $jsonld_cache;
  2805. // normalize local context to an array
  2806. if(is_object($local_ctx) && property_exists($local_ctx, '@context') &&
  2807. is_array($local_ctx->{'@context'})) {
  2808. $local_ctx = $local_ctx->{'@context'};
  2809. }
  2810. $ctxs = self::arrayify($local_ctx);
  2811. // no contexts in array, clone existing context
  2812. if(count($ctxs) === 0) {
  2813. return self::_cloneActiveContext($active_ctx);
  2814. }
  2815. // process each context in order, update active context
  2816. // on each iteration to ensure proper caching
  2817. $rval = $active_ctx;
  2818. foreach($ctxs as $ctx) {
  2819. // reset to initial context
  2820. if($ctx === null) {
  2821. $rval = $active_ctx = $this->_getInitialContext($options);
  2822. continue;
  2823. }
  2824. // dereference @context key if present
  2825. if(is_object($ctx) && property_exists($ctx, '@context')) {
  2826. $ctx = $ctx->{'@context'};
  2827. }
  2828. // context must be an object by now, all URLs retrieved before this call
  2829. if(!is_object($ctx)) {
  2830. throw new JsonLdException(
  2831. 'Invalid JSON-LD syntax; @context must be an object.',
  2832. 'jsonld.SyntaxError', 'invalid local context',
  2833. array('context' => $ctx));
  2834. }
  2835. // get context from cache if available
  2836. if(property_exists($jsonld_cache, 'activeCtx')) {
  2837. $cached = $jsonld_cache->activeCtx->get($active_ctx, $ctx);
  2838. if($cached) {
  2839. $rval = $active_ctx = $cached;
  2840. $must_clone = true;
  2841. continue;
  2842. }
  2843. }
  2844. // update active context and clone new one before updating
  2845. $active_ctx = $rval;
  2846. $rval = self::_cloneActiveContext($rval);
  2847. // define context mappings for keys in local context
  2848. $defined = new stdClass();
  2849. // handle @base
  2850. if(property_exists($ctx, '@base')) {
  2851. $base = $ctx->{'@base'};
  2852. if($base === null) {
  2853. $base = null;
  2854. } else if(!is_string($base)) {
  2855. throw new JsonLdException(
  2856. 'Invalid JSON-LD syntax; the value of "@base" in a ' .
  2857. '@context must be a string or null.',
  2858. 'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx));
  2859. } else if($base !== '' && !self::_isAbsoluteIri($base)) {
  2860. throw new JsonLdException(
  2861. 'Invalid JSON-LD syntax; the value of "@base" in a ' .
  2862. '@context must be an absolute IRI or the empty string.',
  2863. 'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx));
  2864. }
  2865. if($base !== null) {
  2866. $base = jsonld_parse_url($base);
  2867. }
  2868. $rval->{'@base'} = $base;
  2869. $defined->{'@base'} = true;
  2870. }
  2871. // handle @vocab
  2872. if(property_exists($ctx, '@vocab')) {
  2873. $value = $ctx->{'@vocab'};
  2874. if($value === null) {
  2875. unset($rval->{'@vocab'});
  2876. } else if(!is_string($value)) {
  2877. throw new JsonLdException(
  2878. 'Invalid JSON-LD syntax; the value of "@vocab" in a ' .
  2879. '@context must be a string or null.',
  2880. 'jsonld.SyntaxError', 'invalid vocab mapping',
  2881. array('context' => $ctx));
  2882. } else if(!self::_isAbsoluteIri($value)) {
  2883. throw new JsonLdException(
  2884. 'Invalid JSON-LD syntax; the value of "@vocab" in a ' .
  2885. '@context must be an absolute IRI.',
  2886. 'jsonld.SyntaxError', 'invalid vocab mapping',
  2887. array('context' => $ctx));
  2888. } else {
  2889. $rval->{'@vocab'} = $value;
  2890. }
  2891. $defined->{'@vocab'} = true;
  2892. }
  2893. // handle @language
  2894. if(property_exists($ctx, '@language')) {
  2895. $value = $ctx->{'@language'};
  2896. if($value === null) {
  2897. unset($rval->{'@language'});
  2898. } else if(!is_string($value)) {
  2899. throw new JsonLdException(
  2900. 'Invalid JSON-LD syntax; the value of "@language" in a ' .
  2901. '@context must be a string or null.',
  2902. 'jsonld.SyntaxError', 'invalid default language',
  2903. array('context' => $ctx));
  2904. } else {
  2905. $rval->{'@language'} = strtolower($value);
  2906. }
  2907. $defined->{'@language'} = true;
  2908. }
  2909. // process all other keys
  2910. foreach($ctx as $k => $v) {
  2911. $this->_createTermDefinition($rval, $ctx, $k, $defined);
  2912. }
  2913. // cache result
  2914. if(property_exists($jsonld_cache, 'activeCtx')) {
  2915. $jsonld_cache->activeCtx->set($active_ctx, $ctx, $rval);
  2916. }
  2917. }
  2918. return $rval;
  2919. }
  2920. /**
  2921. * Expands a language map.
  2922. *
  2923. * @param stdClass $language_map the language map to expand.
  2924. *
  2925. * @return array the expanded language map.
  2926. */
  2927. protected function _expandLanguageMap($language_map) {
  2928. $rval = array();
  2929. $keys = array_keys((array)$language_map);
  2930. sort($keys);
  2931. foreach($keys as $key) {
  2932. $values = $language_map->{$key};
  2933. $values = self::arrayify($values);
  2934. foreach($values as $item) {
  2935. if($item === null) {
  2936. continue;
  2937. }
  2938. if(!is_string($item)) {
  2939. throw new JsonLdException(
  2940. 'Invalid JSON-LD syntax; language map values must be strings.',
  2941. 'jsonld.SyntaxError', 'invalid language map value',
  2942. array('languageMap', $language_map));
  2943. }
  2944. $rval[] = (object)array(
  2945. '@value' => $item,
  2946. '@language' => strtolower($key));
  2947. }
  2948. }
  2949. return $rval;
  2950. }
  2951. /**
  2952. * Labels the blank nodes in the given value using the given UniqueNamer.
  2953. *
  2954. * @param UniqueNamer $namer the UniqueNamer to use.
  2955. * @param mixed $element the element with blank nodes to rename.
  2956. *
  2957. * @return mixed the element.
  2958. */
  2959. public function _labelBlankNodes($namer, $element) {
  2960. if(is_array($element)) {
  2961. $length = count($element);
  2962. for($i = 0; $i < $length; ++$i) {
  2963. $element[$i] = $this->_labelBlankNodes($namer, $element[$i]);
  2964. }
  2965. } else if(self::_isList($element)) {
  2966. $element->{'@list'} = $this->_labelBlankNodes(
  2967. $namer, $element->{'@list'});
  2968. } else if(is_object($element)) {
  2969. // rename blank node
  2970. if(self::_isBlankNode($element)) {
  2971. $name = null;
  2972. if(property_exists($element, '@id')) {
  2973. $name = $element->{'@id'};
  2974. }
  2975. $element->{'@id'} = $namer->getName($name);
  2976. }
  2977. // recursively apply to all keys
  2978. $keys = array_keys((array)$element);
  2979. sort($keys);
  2980. foreach($keys as $key) {
  2981. if($key !== '@id') {
  2982. $element->{$key} = $this->_labelBlankNodes($namer, $element->{$key});
  2983. }
  2984. }
  2985. }
  2986. return $element;
  2987. }
  2988. /**
  2989. * Expands the given value by using the coercion and keyword rules in the
  2990. * given context.
  2991. *
  2992. * @param stdClass $active_ctx the active context to use.
  2993. * @param string $active_property the property the value is associated with.
  2994. * @param mixed $value the value to expand.
  2995. *
  2996. * @return mixed the expanded value.
  2997. */
  2998. protected function _expandValue($active_ctx, $active_property, $value) {
  2999. // nothing to expand
  3000. if($value === null) {
  3001. return null;
  3002. }
  3003. // special-case expand @id and @type (skips '@id' expansion)
  3004. $expanded_property = $this->_expandIri(
  3005. $active_ctx, $active_property, array('vocab' => true));
  3006. if($expanded_property === '@id') {
  3007. return $this->_expandIri($active_ctx, $value, array('base' => true));
  3008. } else if($expanded_property === '@type') {
  3009. return $this->_expandIri(
  3010. $active_ctx, $value, array('vocab' => true, 'base' => true));
  3011. }
  3012. // get type definition from context
  3013. $type = self::getContextValue($active_ctx, $active_property, '@type');
  3014. // do @id expansion (automatic for @graph)
  3015. if($type === '@id' || ($expanded_property === '@graph' &&
  3016. is_string($value))) {
  3017. return (object)array('@id' => $this->_expandIri(
  3018. $active_ctx, $value, array('base' => true)));
  3019. }
  3020. // do @id expansion w/vocab
  3021. if($type === '@vocab') {
  3022. return (object)array('@id' => $this->_expandIri(
  3023. $active_ctx, $value, array('vocab' => true, 'base' => true)));
  3024. }
  3025. // do not expand keyword values
  3026. if(self::_isKeyword($expanded_property)) {
  3027. return $value;
  3028. }
  3029. $rval = new stdClass();
  3030. // other type
  3031. if($type !== null) {
  3032. $rval->{'@type'} = $type;
  3033. } else if(is_string($value)) {
  3034. // check for language tagging for strings
  3035. $language = self::getContextValue(
  3036. $active_ctx, $active_property, '@language');
  3037. if($language !== null) {
  3038. $rval->{'@language'} = $language;
  3039. }
  3040. }
  3041. $rval->{'@value'} = $value;
  3042. return $rval;
  3043. }
  3044. /**
  3045. * Creates an array of RDF triples for the given graph.
  3046. *
  3047. * @param stdClass $graph the graph to create RDF triples for.
  3048. * @param UniqueNamer $namer for assigning bnode names.
  3049. * @param assoc $options the RDF serialization options.
  3050. *
  3051. * @return array the array of RDF triples for the given graph.
  3052. */
  3053. protected function _graphToRDF($graph, $namer, $options) {
  3054. $rval = array();
  3055. $ids = array_keys((array)$graph);
  3056. sort($ids);
  3057. foreach($ids as $id) {
  3058. $node = $graph->{$id};
  3059. if($id === '"') {
  3060. $id = '';
  3061. }
  3062. $properties = array_keys((array)$node);
  3063. sort($properties);
  3064. foreach($properties as $property) {
  3065. $items = $node->{$property};
  3066. if($property === '@type') {
  3067. $property = self::RDF_TYPE;
  3068. } else if(self::_isKeyword($property)) {
  3069. continue;
  3070. }
  3071. foreach($items as $item) {
  3072. // skip relative IRI subjects and predicates
  3073. if(!(self::_isAbsoluteIri($id) && self::_isAbsoluteIri($property))) {
  3074. continue;
  3075. }
  3076. // RDF subject
  3077. $subject = new stdClass();
  3078. $subject->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI';
  3079. $subject->value = $id;
  3080. // RDF predicate
  3081. $predicate = new stdClass();
  3082. $predicate->type = (strpos($property, '_:') === 0 ?
  3083. 'blank node' : 'IRI');
  3084. $predicate->value = $property;
  3085. // skip bnode predicates unless producing generalized RDF
  3086. if($predicate->type === 'blank node' &&
  3087. !$options['produceGeneralizedRdf']) {
  3088. continue;
  3089. }
  3090. if(self::_isList($item)) {
  3091. // convert @list to triples
  3092. $this->_listToRDF(
  3093. $item->{'@list'}, $namer, $subject, $predicate, $rval);
  3094. } else {
  3095. // convert value or node object to triple
  3096. $object = $this->_objectToRDF($item);
  3097. // skip null objects (they are relative IRIs)
  3098. if($object) {
  3099. $rval[] = (object)array(
  3100. 'subject' => $subject,
  3101. 'predicate' => $predicate,
  3102. 'object' => $object);
  3103. }
  3104. }
  3105. }
  3106. }
  3107. }
  3108. return $rval;
  3109. }
  3110. /**
  3111. * Converts a @list value into linked list of blank node RDF triples
  3112. * (an RDF collection).
  3113. *
  3114. * @param array $list the @list value.
  3115. * @param UniqueNamer $namer for assigning blank node names.
  3116. * @param stdClass $subject the subject for the head of the list.
  3117. * @param stdClass $predicate the predicate for the head of the list.
  3118. * @param &array $triples the array of triples to append to.
  3119. */
  3120. protected function _listToRDF(
  3121. $list, $namer, $subject, $predicate, &$triples) {
  3122. $first = (object)array('type' => 'IRI', 'value' => self::RDF_FIRST);
  3123. $rest = (object)array('type' => 'IRI', 'value' => self::RDF_REST);
  3124. $nil = (object)array('type' => 'IRI', 'value' => self::RDF_NIL);
  3125. foreach($list as $item) {
  3126. $blank_node = (object)array(
  3127. 'type' => 'blank node', 'value' => $namer->getName());
  3128. $triples[] = (object)array(
  3129. 'subject' => $subject,
  3130. 'predicate' => $predicate,
  3131. 'object' => $blank_node);
  3132. $subject = $blank_node;
  3133. $predicate = $first;
  3134. $object = $this->_objectToRDF($item);
  3135. // skip null objects (they are relative IRIs)
  3136. if($object) {
  3137. $triples[] = (object)array(
  3138. 'subject' => $subject,
  3139. 'predicate' => $predicate,
  3140. 'object' => $object);
  3141. }
  3142. $predicate = $rest;
  3143. }
  3144. $triples[] = (object)array(
  3145. 'subject' => $subject, 'predicate' => $predicate, 'object' => $nil);
  3146. }
  3147. /**
  3148. * Converts a JSON-LD value object to an RDF literal or a JSON-LD string or
  3149. * node object to an RDF resource.
  3150. *
  3151. * @param mixed $item the JSON-LD value or node object.
  3152. *
  3153. * @return stdClass the RDF literal or RDF resource.
  3154. */
  3155. protected function _objectToRDF($item) {
  3156. $object = new stdClass();
  3157. if(self::_isValue($item)) {
  3158. $object->type = 'literal';
  3159. $value = $item->{'@value'};
  3160. $datatype = property_exists($item, '@type') ? $item->{'@type'} : null;
  3161. // convert to XSD datatypes as appropriate
  3162. if(is_bool($value)) {
  3163. $object->value = ($value ? 'true' : 'false');
  3164. $object->datatype = $datatype ? $datatype : self::XSD_BOOLEAN;
  3165. } else if(is_double($value) || $datatype == self::XSD_DOUBLE) {
  3166. // canonical double representation
  3167. $object->value = preg_replace(
  3168. '/(\d)0*E\+?/', '$1E', sprintf('%1.15E', $value));
  3169. $object->datatype = $datatype ? $datatype : self::XSD_DOUBLE;
  3170. } else if(is_integer($value)) {
  3171. $object->value = strval($value);
  3172. $object->datatype = $datatype ? $datatype : self::XSD_INTEGER;
  3173. } else if(property_exists($item, '@language')) {
  3174. $object->value = $value;
  3175. $object->datatype = $datatype ? $datatype : self::RDF_LANGSTRING;
  3176. $object->language = $item->{'@language'};
  3177. } else {
  3178. $object->value = $value;
  3179. $object->datatype = $datatype ? $datatype : self::XSD_STRING;
  3180. }
  3181. } else {
  3182. // convert string/node object to RDF
  3183. $id = is_object($item) ? $item->{'@id'} : $item;
  3184. $object->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI';
  3185. $object->value = $id;
  3186. }
  3187. // skip relative IRIs
  3188. if($object->type === 'IRI' && !self::_isAbsoluteIri($object->value)) {
  3189. return null;
  3190. }
  3191. return $object;
  3192. }
  3193. /**
  3194. * Converts an RDF triple object to a JSON-LD object.
  3195. *
  3196. * @param stdClass $o the RDF triple object to convert.
  3197. * @param bool $use_native_types true to output native types, false not to.
  3198. *
  3199. * @return stdClass the JSON-LD object.
  3200. */
  3201. protected function _RDFToObject($o, $use_native_types) {
  3202. // convert IRI/blank node object to JSON-LD
  3203. if($o->type === 'IRI' || $o->type === 'blank node') {
  3204. return (object)array('@id' => $o->value);
  3205. }
  3206. // convert literal object to JSON-LD
  3207. $rval = (object)array('@value' => $o->value);
  3208. if(property_exists($o, 'language')) {
  3209. // add language
  3210. $rval->{'@language'} = $o->language;
  3211. } else {
  3212. // add datatype
  3213. $type = $o->datatype;
  3214. // use native types for certain xsd types
  3215. if($use_native_types) {
  3216. if($type === self::XSD_BOOLEAN) {
  3217. if($rval->{'@value'} === 'true') {
  3218. $rval->{'@value'} = true;
  3219. } else if($rval->{'@value'} === 'false') {
  3220. $rval->{'@value'} = false;
  3221. }
  3222. } else if(is_numeric($rval->{'@value'})) {
  3223. if($type === self::XSD_INTEGER) {
  3224. $i = intval($rval->{'@value'});
  3225. if(strval($i) === $rval->{'@value'}) {
  3226. $rval->{'@value'} = $i;
  3227. }
  3228. } else if($type === self::XSD_DOUBLE) {
  3229. $rval->{'@value'} = doubleval($rval->{'@value'});
  3230. }
  3231. }
  3232. // do not add native type
  3233. if(!in_array($type, array(
  3234. self::XSD_BOOLEAN, self::XSD_INTEGER, self::XSD_DOUBLE,
  3235. self::XSD_STRING))) {
  3236. $rval->{'@type'} = $type;
  3237. }
  3238. } else if($type !== self::XSD_STRING) {
  3239. $rval->{'@type'} = $type;
  3240. }
  3241. }
  3242. return $rval;
  3243. }
  3244. /**
  3245. * Recursively flattens the subjects in the given JSON-LD expanded input
  3246. * into a node map.
  3247. *
  3248. * @param mixed $input the JSON-LD expanded input.
  3249. * @param stdClass $graphs a map of graph name to subject map.
  3250. * @param string $graph the name of the current graph.
  3251. * @param UniqueNamer $namer the blank node namer.
  3252. * @param mixed $name the name assigned to the current input if it is a bnode.
  3253. * @param mixed $list the list to append to, null for none.
  3254. */
  3255. protected function _createNodeMap(
  3256. $input, $graphs, $graph, $namer, $name=null, $list=null) {
  3257. // recurse through array
  3258. if(is_array($input)) {
  3259. foreach($input as $e) {
  3260. $this->_createNodeMap($e, $graphs, $graph, $namer, null, $list);
  3261. }
  3262. return;
  3263. }
  3264. // add non-object to list
  3265. if(!is_object($input)) {
  3266. if($list !== null) {
  3267. $list[] = $input;
  3268. }
  3269. return;
  3270. }
  3271. // add values to list
  3272. if(self::_isValue($input)) {
  3273. if(property_exists($input, '@type')) {
  3274. $type = $input->{'@type'};
  3275. // rename @type blank node
  3276. if(strpos($type, '_:') === 0) {
  3277. $type = $input->{'@type'} = $namer->getName($type);
  3278. }
  3279. }
  3280. if($list !== null) {
  3281. $list[] = $input;
  3282. }
  3283. return;
  3284. }
  3285. // Note: At this point, input must be a subject.
  3286. // spec requires @type to be named first, so assign names early
  3287. if(property_exists($input, '@type')) {
  3288. foreach($input->{'@type'} as $type) {
  3289. if(strpos($type, '_:') === 0) {
  3290. $namer->getName($type);
  3291. }
  3292. }
  3293. }
  3294. // get name for subject
  3295. if($name === null) {
  3296. if(property_exists($input, '@id')) {
  3297. $name = $input->{'@id'};
  3298. }
  3299. if(self::_isBlankNode($input)) {
  3300. $name = $namer->getName($name);
  3301. }
  3302. }
  3303. // add subject reference to list
  3304. if($list !== null) {
  3305. $list[] = (object)array('@id' => $name);
  3306. }
  3307. // create new subject or merge into existing one
  3308. if(!property_exists($graphs, $graph)) {
  3309. $graphs->{$graph} = new stdClass();
  3310. }
  3311. $subjects = $graphs->{$graph};
  3312. if(!property_exists($subjects, $name)) {
  3313. if($name === '') {
  3314. $subjects->{'"'} = new stdClass();
  3315. } else {
  3316. $subjects->{$name} = new stdClass();
  3317. }
  3318. }
  3319. if($name === '') {
  3320. $subject = $subjects->{'"'};
  3321. } else {
  3322. $subject = $subjects->{$name};
  3323. }
  3324. $subject->{'@id'} = $name;
  3325. $properties = array_keys((array)$input);
  3326. sort($properties);
  3327. foreach($properties as $property) {
  3328. // skip @id
  3329. if($property === '@id') {
  3330. continue;
  3331. }
  3332. // handle reverse properties
  3333. if($property === '@reverse') {
  3334. $referenced_node = (object)array('@id' => $name);
  3335. $reverse_map = $input->{'@reverse'};
  3336. foreach($reverse_map as $reverse_property => $items) {
  3337. foreach($items as $item) {
  3338. $item_name = null;
  3339. if(property_exists($item, '@id')) {
  3340. $item_name = $item->{'@id'};
  3341. }
  3342. if(self::_isBlankNode($item)) {
  3343. $item_name = $namer->getName($item_name);
  3344. }
  3345. $this->_createNodeMap($item, $graphs, $graph, $namer, $item_name);
  3346. if($item_name === '') {
  3347. $item_name = '"';
  3348. }
  3349. self::addValue(
  3350. $subjects->{$item_name}, $reverse_property, $referenced_node,
  3351. array('propertyIsArray' => true, 'allowDuplicate' => false));
  3352. }
  3353. }
  3354. continue;
  3355. }
  3356. // recurse into graph
  3357. if($property === '@graph') {
  3358. // add graph subjects map entry
  3359. if(!property_exists($graphs, $name)) {
  3360. // FIXME: temporary hack to avoid empty property bug
  3361. if(!$name) {
  3362. $name = '"';
  3363. }
  3364. $graphs->{$name} = new stdClass();
  3365. }
  3366. $g = ($graph === '@merged') ? $graph : $name;
  3367. $this->_createNodeMap(
  3368. $input->{$property}, $graphs, $g, $namer, null, null);
  3369. continue;
  3370. }
  3371. // copy non-@type keywords
  3372. if($property !== '@type' && self::_isKeyword($property)) {
  3373. if($property === '@index' && property_exists($subject, '@index') &&
  3374. ($input->{'@index'} !== $subject->{'@index'} ||
  3375. $input->{'@index'}->{'@id'} !== $subject->{'@index'}->{'@id'})) {
  3376. throw new JsonLdException(
  3377. 'Invalid JSON-LD syntax; conflicting @index property detected.',
  3378. 'jsonld.SyntaxError', 'conflicting indexes',
  3379. array('subject' => $subject));
  3380. }
  3381. $subject->{$property} = $input->{$property};
  3382. continue;
  3383. }
  3384. // iterate over objects
  3385. $objects = $input->{$property};
  3386. // if property is a bnode, assign it a new id
  3387. if(strpos($property, '_:') === 0) {
  3388. $property = $namer->getName($property);
  3389. }
  3390. // ensure property is added for empty arrays
  3391. if(count($objects) === 0) {
  3392. self::addValue(
  3393. $subject, $property, array(), array('propertyIsArray' => true));
  3394. continue;
  3395. }
  3396. foreach($objects as $o) {
  3397. if($property === '@type') {
  3398. // rename @type blank nodes
  3399. $o = (strpos($o, '_:') === 0) ? $namer->getName($o) : $o;
  3400. }
  3401. // handle embedded subject or subject reference
  3402. if(self::_isSubject($o) || self::_isSubjectReference($o)) {
  3403. // rename blank node @id
  3404. $id = property_exists($o, '@id') ? $o->{'@id'} : null;
  3405. if(self::_isBlankNode($o)) {
  3406. $id = $namer->getName($id);
  3407. }
  3408. // add reference and recurse
  3409. self::addValue(
  3410. $subject, $property, (object)array('@id' => $id),
  3411. array('propertyIsArray' => true, 'allowDuplicate' => false));
  3412. $this->_createNodeMap($o, $graphs, $graph, $namer, $id, null);
  3413. } else if(self::_isList($o)) {
  3414. // handle @list
  3415. $_list = new ArrayObject();
  3416. $this->_createNodeMap(
  3417. $o->{'@list'}, $graphs, $graph, $namer, $name, $_list);
  3418. $o = (object)array('@list' => (array)$_list);
  3419. self::addValue(
  3420. $subject, $property, $o,
  3421. array('propertyIsArray' => true, 'allowDuplicate' => false));
  3422. } else {
  3423. // handle @value
  3424. $this->_createNodeMap($o, $graphs, $graph, $namer, $name, null);
  3425. self::addValue(
  3426. $subject, $property, $o,
  3427. array('propertyIsArray' => true, 'allowDuplicate' => false));
  3428. }
  3429. }
  3430. }
  3431. }
  3432. /**
  3433. * Frames subjects according to the given frame.
  3434. *
  3435. * @param stdClass $state the current framing state.
  3436. * @param array $subjects the subjects to filter.
  3437. * @param array $frame the frame.
  3438. * @param mixed $parent the parent subject or top-level array.
  3439. * @param mixed $property the parent property, initialized to null.
  3440. */
  3441. protected function _matchFrame(
  3442. $state, $subjects, $frame, $parent, $property) {
  3443. // validate the frame
  3444. $this->_validateFrame($frame);
  3445. $frame = $frame[0];
  3446. // get flags for current frame
  3447. $options = $state->options;
  3448. $flags = array(
  3449. 'embed' => $this->_getFrameFlag($frame, $options, 'embed'),
  3450. 'explicit' => $this->_getFrameFlag($frame, $options, 'explicit'),
  3451. 'requireAll' => $this->_getFrameFlag($frame, $options, 'requireAll'));
  3452. // filter out subjects that match the frame
  3453. $matches = $this->_filterSubjects($state, $subjects, $frame, $flags);
  3454. // add matches to output
  3455. foreach($matches as $id => $subject) {
  3456. if($flags['embed'] === '@link' && property_exists($state->link, $id)) {
  3457. // TODO: may want to also match an existing linked subject against
  3458. // the current frame ... so different frames could produce different
  3459. // subjects that are only shared in-memory when the frames are the same
  3460. // add existing linked subject
  3461. $this->_addFrameOutput($parent, $property, $state->link->{$id});
  3462. continue;
  3463. }
  3464. /* Note: In order to treat each top-level match as a compartmentalized
  3465. result, clear the unique embedded subjects map when the property is null,
  3466. which only occurs at the top-level. */
  3467. if($property === null) {
  3468. $state->uniqueEmbeds = new stdClass();
  3469. }
  3470. // start output for subject
  3471. $output = new stdClass();
  3472. $output->{'@id'} = $id;
  3473. $state->link->{$id} = $output;
  3474. // if embed is @never or if a circular reference would be created by an
  3475. // embed, the subject cannot be embedded, just add the reference;
  3476. // note that a circular reference won't occur when the embed flag is
  3477. // `@link` as the above check will short-circuit before reaching this point
  3478. if($flags['embed'] === '@never' ||
  3479. $this->_createsCircularReference($subject, $state->subjectStack)) {
  3480. $this->_addFrameOutput($parent, $property, $output);
  3481. continue;
  3482. }
  3483. // if only the last match should be embedded
  3484. if($flags['embed'] === '@last') {
  3485. // remove any existing embed
  3486. if(property_exists($state->uniqueEmbeds, $id)) {
  3487. $this->_removeEmbed($state, $id);
  3488. }
  3489. $state->uniqueEmbeds->{$id} = array(
  3490. 'parent' => $parent, 'property' => $property);
  3491. }
  3492. // push matching subject onto stack to enable circular embed checks
  3493. $state->subjectStack[] = $subject;
  3494. // iterate over subject properties
  3495. $props = array_keys((array)$subject);
  3496. sort($props);
  3497. foreach($props as $prop) {
  3498. // copy keywords to output
  3499. if(self::_isKeyword($prop)) {
  3500. $output->{$prop} = self::copy($subject->{$prop});
  3501. continue;
  3502. }
  3503. // explicit is on and property isn't in the frame, skip processing
  3504. if($flags['explicit'] && !property_exists($frame, $prop)) {
  3505. continue;
  3506. }
  3507. // add objects
  3508. $objects = $subject->{$prop};
  3509. foreach($objects as $o) {
  3510. // recurse into list
  3511. if(self::_isList($o)) {
  3512. // add empty list
  3513. $list = (object)array('@list' => array());
  3514. $this->_addFrameOutput($output, $prop, $list);
  3515. // add list objects
  3516. $src = $o->{'@list'};
  3517. foreach($src as $o) {
  3518. if(self::_isSubjectReference($o)) {
  3519. // recurse into subject reference
  3520. $subframe = (property_exists($frame, $prop) ?
  3521. $frame->{$prop}[0]->{'@list'} :
  3522. $this->_createImplicitFrame($flags));
  3523. $this->_matchFrame(
  3524. $state, array($o->{'@id'}), $subframe, $list, '@list');
  3525. } else {
  3526. // include other values automatically
  3527. $this->_addFrameOutput($list, '@list', self::copy($o));
  3528. }
  3529. }
  3530. continue;
  3531. }
  3532. if(self::_isSubjectReference($o)) {
  3533. // recurse into subject reference
  3534. $subframe = (property_exists($frame, $prop) ?
  3535. $frame->{$prop} : $this->_createImplicitFrame($flags));
  3536. $this->_matchFrame(
  3537. $state, array($o->{'@id'}), $subframe, $output, $prop);
  3538. } else {
  3539. // include other values automatically
  3540. $this->_addFrameOutput($output, $prop, self::copy($o));
  3541. }
  3542. }
  3543. }
  3544. // handle defaults
  3545. $props = array_keys((array)$frame);
  3546. sort($props);
  3547. foreach($props as $prop) {
  3548. // skip keywords
  3549. if(self::_isKeyword($prop)) {
  3550. continue;
  3551. }
  3552. // if omit default is off, then include default values for properties
  3553. // that appear in the next frame but are not in the matching subject
  3554. $next = $frame->{$prop}[0];
  3555. $omit_default_on = $this->_getFrameFlag(
  3556. $next, $options, 'omitDefault');
  3557. if(!$omit_default_on && !property_exists($output, $prop)) {
  3558. $preserve = '@null';
  3559. if(property_exists($next, '@default')) {
  3560. $preserve = self::copy($next->{'@default'});
  3561. }
  3562. $preserve = self::arrayify($preserve);
  3563. $output->{$prop} = array((object)array('@preserve' => $preserve));
  3564. }
  3565. }
  3566. // add output to parent
  3567. $this->_addFrameOutput($parent, $property, $output);
  3568. // pop matching subject from circular ref-checking stack
  3569. array_pop($state->subjectStack);
  3570. }
  3571. }
  3572. /**
  3573. * Creates an implicit frame when recursing through subject matches. If
  3574. * a frame doesn't have an explicit frame for a particular property, then
  3575. * a wildcard child frame will be created that uses the same flags that the
  3576. * parent frame used.
  3577. *
  3578. * @param assoc flags the current framing flags.
  3579. *
  3580. * @return array the implicit frame.
  3581. */
  3582. function _createImplicitFrame($flags) {
  3583. $frame = new stdClass();
  3584. foreach($flags as $key => $value) {
  3585. $frame->{'@' . $key} = array($flags[$key]);
  3586. }
  3587. return array($frame);
  3588. }
  3589. /**
  3590. * Checks the current subject stack to see if embedding the given subject
  3591. * would cause a circular reference.
  3592. *
  3593. * @param stdClass subject_to_embed the subject to embed.
  3594. * @param assoc subject_stack the current stack of subjects.
  3595. *
  3596. * @return bool true if a circular reference would be created, false if not.
  3597. */
  3598. function _createsCircularReference($subject_to_embed, $subject_stack) {
  3599. for($i = count($subject_stack) - 1; $i >= 0; --$i) {
  3600. if($subject_stack[$i]->{'@id'} === $subject_to_embed->{'@id'}) {
  3601. return true;
  3602. }
  3603. }
  3604. return false;
  3605. }
  3606. /**
  3607. * Gets the frame flag value for the given flag name.
  3608. *
  3609. * @param stdClass $frame the frame.
  3610. * @param stdClass $options the framing options.
  3611. * @param string $name the flag name.
  3612. *
  3613. * @return mixed $the flag value.
  3614. */
  3615. protected function _getFrameFlag($frame, $options, $name) {
  3616. $flag = "@$name";
  3617. $rval = (property_exists($frame, $flag) ?
  3618. $frame->{$flag}[0] : $options[$name]);
  3619. if($name === 'embed') {
  3620. // default is "@last"
  3621. // backwards-compatibility support for "embed" maps:
  3622. // true => "@last"
  3623. // false => "@never"
  3624. if($rval === true) {
  3625. $rval = '@last';
  3626. } else if($rval === false) {
  3627. $rval = '@never';
  3628. } else if($rval !== '@always' && $rval !== '@never' &&
  3629. $rval !== '@link') {
  3630. $rval = '@last';
  3631. }
  3632. }
  3633. return $rval;
  3634. }
  3635. /**
  3636. * Validates a JSON-LD frame, throwing an exception if the frame is invalid.
  3637. *
  3638. * @param array $frame the frame to validate.
  3639. */
  3640. protected function _validateFrame($frame) {
  3641. if(!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) {
  3642. throw new JsonLdException(
  3643. 'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.',
  3644. 'jsonld.SyntaxError', null, array('frame' => $frame));
  3645. }
  3646. }
  3647. /**
  3648. * Returns a map of all of the subjects that match a parsed frame.
  3649. *
  3650. * @param stdClass $state the current framing state.
  3651. * @param array $subjects the set of subjects to filter.
  3652. * @param stdClass $frame the parsed frame.
  3653. * @param assoc $flags the frame flags.
  3654. *
  3655. * @return stdClass all of the matched subjects.
  3656. */
  3657. protected function _filterSubjects($state, $subjects, $frame, $flags) {
  3658. $rval = new stdClass();
  3659. sort($subjects);
  3660. foreach($subjects as $id) {
  3661. $subject = $state->subjects->{$id};
  3662. if($this->_filterSubject($subject, $frame, $flags)) {
  3663. $rval->{$id} = $subject;
  3664. }
  3665. }
  3666. return $rval;
  3667. }
  3668. /**
  3669. * Returns true if the given subject matches the given frame.
  3670. *
  3671. * @param stdClass $subject the subject to check.
  3672. * @param stdClass $frame the frame to check.
  3673. * @param assoc $flags the frame flags.
  3674. *
  3675. * @return bool true if the subject matches, false if not.
  3676. */
  3677. protected function _filterSubject($subject, $frame, $flags) {
  3678. // check @type (object value means 'any' type, fall through to ducktyping)
  3679. if(property_exists($frame, '@type') &&
  3680. !(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) {
  3681. $types = $frame->{'@type'};
  3682. foreach($types as $type) {
  3683. // any matching @type is a match
  3684. if(self::hasValue($subject, '@type', $type)) {
  3685. return true;
  3686. }
  3687. }
  3688. return false;
  3689. }
  3690. // check ducktype
  3691. $wildcard = true;
  3692. $matches_some = false;
  3693. foreach($frame as $k => $v) {
  3694. if(self::_isKeyword($k)) {
  3695. // skip non-@id and non-@type
  3696. if($k !== '@id' && $k !== '@type') {
  3697. continue;
  3698. }
  3699. $wildcard = false;
  3700. // check @id for a specific @id value
  3701. if($k === '@id' && is_string($v)) {
  3702. if(!property_exists($subject, $k) || $subject->{$k} !== $v) {
  3703. return false;
  3704. }
  3705. $matches_some = true;
  3706. continue;
  3707. }
  3708. }
  3709. $wildcard = false;
  3710. if(property_exists($subject, $k)) {
  3711. // $v === [] means do not match if property is present
  3712. if(is_array($v) && count($v) === 0) {
  3713. return false;
  3714. }
  3715. $matches_some = true;
  3716. continue;
  3717. }
  3718. // all properties must match to be a duck unless a @default is specified
  3719. $has_default = (is_array($v) && count($v) === 1 && is_object($v[0]) &&
  3720. property_exists($v[0], '@default'));
  3721. if($flags['requireAll'] && !$has_default) {
  3722. return false;
  3723. }
  3724. }
  3725. // return true if wildcard or subject matches some properties
  3726. return $wildcard || $matches_some;
  3727. }
  3728. /**
  3729. * Removes an existing embed.
  3730. *
  3731. * @param stdClass $state the current framing state.
  3732. * @param string $id the @id of the embed to remove.
  3733. */
  3734. protected function _removeEmbed($state, $id) {
  3735. // get existing embed
  3736. $embeds = $state->uniqueEmbeds;
  3737. $embed = $embeds->{$id};
  3738. $property = $embed['property'];
  3739. // create reference to replace embed
  3740. $subject = (object)array('@id' => $id);
  3741. // remove existing embed
  3742. if(is_array($embed->parent)) {
  3743. // replace subject with reference
  3744. foreach($embed->parent as $i => $parent) {
  3745. if(self::compareValues($parent, $subject)) {
  3746. $embed->parent[$i] = $subject;
  3747. break;
  3748. }
  3749. }
  3750. } else {
  3751. // replace subject with reference
  3752. $use_array = is_array($embed->parent->{$property});
  3753. self::removeValue($embed->parent, $property, $subject,
  3754. array('propertyIsArray' => $use_array));
  3755. self::addValue($embed->parent, $property, $subject,
  3756. array('propertyIsArray' => $use_array));
  3757. }
  3758. // recursively remove dependent dangling embeds
  3759. $removeDependents = function($id) {
  3760. // get embed keys as a separate array to enable deleting keys in map
  3761. $ids = array_keys((array)$embeds);
  3762. foreach($ids as $next) {
  3763. if(property_exists($embeds, $next) &&
  3764. is_object($embeds->{$next}->parent) &&
  3765. $embeds->{$next}->parent->{'@id'} === $id) {
  3766. unset($embeds->{$next});
  3767. $removeDependents($next);
  3768. }
  3769. }
  3770. };
  3771. $removeDependents($id);
  3772. }
  3773. /**
  3774. * Adds framing output to the given parent.
  3775. *
  3776. * @param mixed $parent the parent to add to.
  3777. * @param string $property the parent property.
  3778. * @param mixed $output the output to add.
  3779. */
  3780. protected function _addFrameOutput($parent, $property, $output) {
  3781. if(is_object($parent) && !($parent instanceof ArrayObject)) {
  3782. self::addValue(
  3783. $parent, $property, $output, array('propertyIsArray' => true));
  3784. } else {
  3785. $parent[] = $output;
  3786. }
  3787. }
  3788. /**
  3789. * Removes the @preserve keywords as the last step of the framing algorithm.
  3790. *
  3791. * @param stdClass $ctx the active context used to compact the input.
  3792. * @param mixed $input the framed, compacted output.
  3793. * @param assoc $options the compaction options used.
  3794. *
  3795. * @return mixed the resulting output.
  3796. */
  3797. protected function _removePreserve($ctx, $input, $options) {
  3798. // recurse through arrays
  3799. if(is_array($input)) {
  3800. $output = array();
  3801. foreach($input as $e) {
  3802. $result = $this->_removePreserve($ctx, $e, $options);
  3803. // drop nulls from arrays
  3804. if($result !== null) {
  3805. $output[] = $result;
  3806. }
  3807. }
  3808. $input = $output;
  3809. } else if(is_object($input)) {
  3810. // remove @preserve
  3811. if(property_exists($input, '@preserve')) {
  3812. if($input->{'@preserve'} === '@null') {
  3813. return null;
  3814. }
  3815. return $input->{'@preserve'};
  3816. }
  3817. // skip @values
  3818. if(self::_isValue($input)) {
  3819. return $input;
  3820. }
  3821. // recurse through @lists
  3822. if(self::_isList($input)) {
  3823. $input->{'@list'} = $this->_removePreserve(
  3824. $ctx, $input->{'@list'}, $options);
  3825. return $input;
  3826. }
  3827. // handle in-memory linked nodes
  3828. $id_alias = $this->_compactIri($ctx, '@id');
  3829. if(property_exists($input, $id_alias)) {
  3830. $id = $input->{$id_alias};
  3831. if(isset($options['link'][$id])) {
  3832. $idx = array_search($input, $options['link'][$id]);
  3833. if($idx === false) {
  3834. // prevent circular visitation
  3835. $options['link'][$id][] = $input;
  3836. } else {
  3837. // already visited
  3838. return $options['link'][$id][$idx];
  3839. }
  3840. } else {
  3841. // prevent circular visitation
  3842. $options['link'][$id] = array($input);
  3843. }
  3844. }
  3845. // recurse through properties
  3846. foreach($input as $prop => $v) {
  3847. $result = $this->_removePreserve($ctx, $v, $options);
  3848. $container = self::getContextValue($ctx, $prop, '@container');
  3849. if($options['compactArrays'] &&
  3850. is_array($result) && count($result) === 1 &&
  3851. $container !== '@set' && $container !== '@list') {
  3852. $result = $result[0];
  3853. }
  3854. $input->{$prop} = $result;
  3855. }
  3856. }
  3857. return $input;
  3858. }
  3859. /**
  3860. * Compares two RDF triples for equality.
  3861. *
  3862. * @param stdClass $t1 the first triple.
  3863. * @param stdClass $t2 the second triple.
  3864. *
  3865. * @return true if the triples are the same, false if not.
  3866. */
  3867. protected static function _compareRDFTriples($t1, $t2) {
  3868. foreach(array('subject', 'predicate', 'object') as $attr) {
  3869. if($t1->{$attr}->type !== $t2->{$attr}->type ||
  3870. $t1->{$attr}->value !== $t2->{$attr}->value) {
  3871. return false;
  3872. }
  3873. }
  3874. if(property_exists($t1->object, 'language') !==
  3875. property_exists($t1->object, 'language')) {
  3876. return false;
  3877. }
  3878. if(property_exists($t1->object, 'language') &&
  3879. $t1->object->language !== $t2->object->language) {
  3880. return false;
  3881. }
  3882. if(property_exists($t1->object, 'datatype') &&
  3883. $t1->object->datatype !== $t2->object->datatype) {
  3884. return false;
  3885. }
  3886. return true;
  3887. }
  3888. /**
  3889. * Hashes all of the quads about a blank node.
  3890. *
  3891. * @param string $id the ID of the bnode to hash quads for.
  3892. * @param stdClass $bnodes the mapping of bnodes to quads.
  3893. * @param UniqueNamer $namer the canonical bnode namer.
  3894. *
  3895. * @return string the new hash.
  3896. */
  3897. protected function _hashQuads($id, $bnodes, $namer) {
  3898. // return cached hash
  3899. if(property_exists($bnodes->{$id}, 'hash')) {
  3900. return $bnodes->{$id}->hash;
  3901. }
  3902. // serialize all of bnode's quads
  3903. $quads = $bnodes->{$id}->quads;
  3904. $nquads = array();
  3905. foreach($quads as $quad) {
  3906. $nquads[] = $this->toNQuad($quad, property_exists($quad, 'name') ?
  3907. $quad->name->value : null, $id);
  3908. }
  3909. // sort serialized quads
  3910. sort($nquads);
  3911. // cache and return hashed quads
  3912. $hash = $bnodes->{$id}->hash = sha1(implode($nquads));
  3913. return $hash;
  3914. }
  3915. /**
  3916. * Produces a hash for the paths of adjacent bnodes for a bnode,
  3917. * incorporating all information about its subgraph of bnodes. This
  3918. * method will recursively pick adjacent bnode permutations that produce the
  3919. * lexicographically-least 'path' serializations.
  3920. *
  3921. * @param string $id the ID of the bnode to hash paths for.
  3922. * @param stdClass $bnodes the map of bnode quads.
  3923. * @param UniqueNamer $namer the canonical bnode namer.
  3924. * @param UniqueNamer $path_namer the namer used to assign names to adjacent
  3925. * bnodes.
  3926. *
  3927. * @return stdClass the hash and path namer used.
  3928. */
  3929. protected function _hashPaths($id, $bnodes, $namer, $path_namer) {
  3930. // create SHA-1 digest
  3931. $md = hash_init('sha1');
  3932. // group adjacent bnodes by hash, keep properties and references separate
  3933. $groups = new stdClass();
  3934. $quads = $bnodes->{$id}->quads;
  3935. foreach($quads as $quad) {
  3936. // get adjacent bnode
  3937. $bnode = $this->_getAdjacentBlankNodeName($quad->subject, $id);
  3938. if($bnode !== null) {
  3939. // normal property
  3940. $direction = 'p';
  3941. } else {
  3942. $bnode = $this->_getAdjacentBlankNodeName($quad->object, $id);
  3943. if($bnode !== null) {
  3944. // reverse property
  3945. $direction = 'r';
  3946. }
  3947. }
  3948. if($bnode !== null) {
  3949. // get bnode name (try canonical, path, then hash)
  3950. if($namer->isNamed($bnode)) {
  3951. $name = $namer->getName($bnode);
  3952. } else if($path_namer->isNamed($bnode)) {
  3953. $name = $path_namer->getName($bnode);
  3954. } else {
  3955. $name = $this->_hashQuads($bnode, $bnodes, $namer);
  3956. }
  3957. // hash direction, property, and bnode name/hash
  3958. $group_md = hash_init('sha1');
  3959. hash_update($group_md, $direction);
  3960. hash_update($group_md, $quad->predicate->value);
  3961. hash_update($group_md, $name);
  3962. $group_hash = hash_final($group_md);
  3963. // add bnode to hash group
  3964. if(property_exists($groups, $group_hash)) {
  3965. $groups->{$group_hash}[] = $bnode;
  3966. } else {
  3967. $groups->{$group_hash} = array($bnode);
  3968. }
  3969. }
  3970. }
  3971. // iterate over groups in sorted hash order
  3972. $group_hashes = array_keys((array)$groups);
  3973. sort($group_hashes);
  3974. foreach($group_hashes as $group_hash) {
  3975. // digest group hash
  3976. hash_update($md, $group_hash);
  3977. // choose a path and namer from the permutations
  3978. $chosen_path = null;
  3979. $chosen_namer = null;
  3980. $permutator = new Permutator($groups->{$group_hash});
  3981. while($permutator->hasNext()) {
  3982. $permutation = $permutator->next();
  3983. $path_namer_copy = clone $path_namer;
  3984. // build adjacent path
  3985. $path = '';
  3986. $skipped = false;
  3987. $recurse = array();
  3988. foreach($permutation as $bnode) {
  3989. // use canonical name if available
  3990. if($namer->isNamed($bnode)) {
  3991. $path .= $namer->getName($bnode);
  3992. } else {
  3993. // recurse if bnode isn't named in the path yet
  3994. if(!$path_namer_copy->isNamed($bnode)) {
  3995. $recurse[] = $bnode;
  3996. }
  3997. $path .= $path_namer_copy->getName($bnode);
  3998. }
  3999. // skip permutation if path is already >= chosen path
  4000. if($chosen_path !== null && strlen($path) >= strlen($chosen_path) &&
  4001. $path > $chosen_path) {
  4002. $skipped = true;
  4003. break;
  4004. }
  4005. }
  4006. // recurse
  4007. if(!$skipped) {
  4008. foreach($recurse as $bnode) {
  4009. $result = $this->_hashPaths(
  4010. $bnode, $bnodes, $namer, $path_namer_copy);
  4011. $path .= $path_namer_copy->getName($bnode);
  4012. $path .= "<{$result->hash}>";
  4013. $path_namer_copy = $result->pathNamer;
  4014. // skip permutation if path is already >= chosen path
  4015. if($chosen_path !== null &&
  4016. strlen($path) >= strlen($chosen_path) && $path > $chosen_path) {
  4017. $skipped = true;
  4018. break;
  4019. }
  4020. }
  4021. }
  4022. if(!$skipped && ($chosen_path === null || $path < $chosen_path)) {
  4023. $chosen_path = $path;
  4024. $chosen_namer = $path_namer_copy;
  4025. }
  4026. }
  4027. // digest chosen path and update namer
  4028. hash_update($md, $chosen_path);
  4029. $path_namer = $chosen_namer;
  4030. }
  4031. // return SHA-1 hash and path namer
  4032. return (object)array(
  4033. 'hash' => hash_final($md), 'pathNamer' => $path_namer);
  4034. }
  4035. /**
  4036. * A helper function that gets the blank node name from an RDF quad
  4037. * node (subject or object). If the node is not a blank node or its
  4038. * value does not match the given blank node ID, it will be returned.
  4039. *
  4040. * @param stdClass $node the RDF quad node.
  4041. * @param string $id the ID of the blank node to look next to.
  4042. *
  4043. * @return mixed the adjacent blank node name or null if none was found.
  4044. */
  4045. protected function _getAdjacentBlankNodeName($node, $id) {
  4046. if($node->type === 'blank node' && $node->value !== $id) {
  4047. return $node->value;
  4048. }
  4049. return null;
  4050. }
  4051. /**
  4052. * Compares two strings first based on length and then lexicographically.
  4053. *
  4054. * @param string $a the first string.
  4055. * @param string $b the second string.
  4056. *
  4057. * @return integer -1 if a < b, 1 if a > b, 0 if a == b.
  4058. */
  4059. protected function _compareShortestLeast($a, $b) {
  4060. $len_a = strlen($a);
  4061. $len_b = strlen($b);
  4062. if($len_a < $len_b) {
  4063. return -1;
  4064. }
  4065. if($len_b < $len_a) {
  4066. return 1;
  4067. }
  4068. if($a === $b) {
  4069. return 0;
  4070. }
  4071. return ($a < $b) ? -1 : 1;
  4072. }
  4073. /**
  4074. * Picks the preferred compaction term from the given inverse context entry.
  4075. *
  4076. * @param active_ctx the active context.
  4077. * @param iri the IRI to pick the term for.
  4078. * @param value the value to pick the term for.
  4079. * @param containers the preferred containers.
  4080. * @param type_or_language either '@type' or '@language'.
  4081. * @param type_or_language_value the preferred value for '@type' or
  4082. * '@language'.
  4083. *
  4084. * @return mixed the preferred term.
  4085. */
  4086. protected function _selectTerm(
  4087. $active_ctx, $iri, $value, $containers,
  4088. $type_or_language, $type_or_language_value) {
  4089. if($type_or_language_value === null) {
  4090. $type_or_language_value = '@null';
  4091. }
  4092. // options for the value of @type or @language
  4093. $prefs = array();
  4094. // determine prefs for @id based on whether or not value compacts to a term
  4095. if(($type_or_language_value === '@id' ||
  4096. $type_or_language_value === '@reverse') &&
  4097. self::_isSubjectReference($value)) {
  4098. // prefer @reverse first
  4099. if($type_or_language_value === '@reverse') {
  4100. $prefs[] = '@reverse';
  4101. }
  4102. // try to compact value to a term
  4103. $term = $this->_compactIri(
  4104. $active_ctx, $value->{'@id'}, null, array('vocab' => true));
  4105. if(property_exists($active_ctx->mappings, $term) &&
  4106. $active_ctx->mappings->{$term} &&
  4107. $active_ctx->mappings->{$term}->{'@id'} === $value->{'@id'}) {
  4108. // prefer @vocab
  4109. array_push($prefs, '@vocab', '@id');
  4110. } else {
  4111. // prefer @id
  4112. array_push($prefs, '@id', '@vocab');
  4113. }
  4114. } else {
  4115. $prefs[] = $type_or_language_value;
  4116. }
  4117. $prefs[] = '@none';
  4118. $container_map = $active_ctx->inverse->{$iri};
  4119. foreach($containers as $container) {
  4120. // if container not available in the map, continue
  4121. if(!property_exists($container_map, $container)) {
  4122. continue;
  4123. }
  4124. $type_or_language_value_map =
  4125. $container_map->{$container}->{$type_or_language};
  4126. foreach($prefs as $pref) {
  4127. // if type/language option not available in the map, continue
  4128. if(!property_exists($type_or_language_value_map, $pref)) {
  4129. continue;
  4130. }
  4131. // select term
  4132. return $type_or_language_value_map->{$pref};
  4133. }
  4134. }
  4135. return null;
  4136. }
  4137. /**
  4138. * Compacts an IRI or keyword into a term or prefix if it can be. If the
  4139. * IRI has an associated value it may be passed.
  4140. *
  4141. * @param stdClass $active_ctx the active context to use.
  4142. * @param string $iri the IRI to compact.
  4143. * @param mixed $value the value to check or null.
  4144. * @param assoc $relative_to options for how to compact IRIs:
  4145. * vocab: true to split after @vocab, false not to.
  4146. * @param bool $reverse true if a reverse property is being compacted, false
  4147. * if not.
  4148. *
  4149. * @return string the compacted term, prefix, keyword alias, or original IRI.
  4150. */
  4151. protected function _compactIri(
  4152. $active_ctx, $iri, $value=null, $relative_to=array(), $reverse=false) {
  4153. // can't compact null
  4154. if($iri === null) {
  4155. return $iri;
  4156. }
  4157. $inverse_ctx = $this->_getInverseContext($active_ctx);
  4158. if(self::_isKeyword($iri)) {
  4159. // a keyword can only be compacted to simple alias
  4160. if(property_exists($inverse_ctx, $iri)) {
  4161. return $inverse_ctx->$iri->{'@none'}->{'@type'}->{'@none'};
  4162. }
  4163. return $iri;
  4164. }
  4165. if(!isset($relative_to['vocab'])) {
  4166. $relative_to['vocab'] = false;
  4167. }
  4168. // use inverse context to pick a term if iri is relative to vocab
  4169. if($relative_to['vocab'] && property_exists($inverse_ctx, $iri)) {
  4170. $default_language = '@none';
  4171. if(property_exists($active_ctx, '@language')) {
  4172. $default_language = $active_ctx->{'@language'};
  4173. }
  4174. // prefer @index if available in value
  4175. $containers = array();
  4176. if(is_object($value) && property_exists($value, '@index')) {
  4177. $containers[] = '@index';
  4178. }
  4179. // defaults for term selection based on type/language
  4180. $type_or_language = '@language';
  4181. $type_or_language_value = '@null';
  4182. if($reverse) {
  4183. $type_or_language = '@type';
  4184. $type_or_language_value = '@reverse';
  4185. $containers[] = '@set';
  4186. } else if(self::_isList($value)) {
  4187. // choose the most specific term that works for all elements in @list
  4188. // only select @list containers if @index is NOT in value
  4189. if(!property_exists($value, '@index')) {
  4190. $containers[] = '@list';
  4191. }
  4192. $list = $value->{'@list'};
  4193. $common_language = (count($list) === 0) ? $default_language : null;
  4194. $common_type = null;
  4195. foreach($list as $item) {
  4196. $item_language = '@none';
  4197. $item_type = '@none';
  4198. if(self::_isValue($item)) {
  4199. if(property_exists($item, '@language')) {
  4200. $item_language = $item->{'@language'};
  4201. } else if(property_exists($item, '@type')) {
  4202. $item_type = $item->{'@type'};
  4203. } else {
  4204. // plain literal
  4205. $item_language = '@null';
  4206. }
  4207. } else {
  4208. $item_type = '@id';
  4209. }
  4210. if($common_language === null) {
  4211. $common_language = $item_language;
  4212. } else if($item_language !== $common_language &&
  4213. self::_isValue($item)) {
  4214. $common_language = '@none';
  4215. }
  4216. if($common_type === null) {
  4217. $common_type = $item_type;
  4218. } else if($item_type !== $common_type) {
  4219. $common_type = '@none';
  4220. }
  4221. // there are different languages and types in the list, so choose
  4222. // the most generic term, no need to keep iterating the list
  4223. if($common_language === '@none' && $common_type === '@none') {
  4224. break;
  4225. }
  4226. }
  4227. if($common_language === null) {
  4228. $common_language = '@none';
  4229. }
  4230. if($common_type === null) {
  4231. $common_type = '@none';
  4232. }
  4233. if($common_type !== '@none') {
  4234. $type_or_language = '@type';
  4235. $type_or_language_value = $common_type;
  4236. } else {
  4237. $type_or_language_value = $common_language;
  4238. }
  4239. } else {
  4240. if(self::_isValue($value)) {
  4241. if(property_exists($value, '@language') &&
  4242. !property_exists($value, '@index')) {
  4243. $containers[] = '@language';
  4244. $type_or_language_value = $value->{'@language'};
  4245. } else if(property_exists($value, '@type')) {
  4246. $type_or_language = '@type';
  4247. $type_or_language_value = $value->{'@type'};
  4248. }
  4249. } else {
  4250. $type_or_language = '@type';
  4251. $type_or_language_value = '@id';
  4252. }
  4253. $containers[] = '@set';
  4254. }
  4255. // do term selection
  4256. $containers[] = '@none';
  4257. $term = $this->_selectTerm(
  4258. $active_ctx, $iri, $value,
  4259. $containers, $type_or_language, $type_or_language_value);
  4260. if($term !== null) {
  4261. return $term;
  4262. }
  4263. }
  4264. // no term match, use @vocab if available
  4265. if($relative_to['vocab']) {
  4266. if(property_exists($active_ctx, '@vocab')) {
  4267. // determine if vocab is a prefix of the iri
  4268. $vocab = $active_ctx->{'@vocab'};
  4269. if(strpos($iri, $vocab) === 0 && $iri !== $vocab) {
  4270. // use suffix as relative iri if it is not a term in the active
  4271. // context
  4272. $suffix = substr($iri, strlen($vocab));
  4273. if(!property_exists($active_ctx->mappings, $suffix)) {
  4274. return $suffix;
  4275. }
  4276. }
  4277. }
  4278. }
  4279. // no term or @vocab match, check for possible CURIEs
  4280. $choice = null;
  4281. $idx = 0;
  4282. $partial_matches = array();
  4283. $iri_map = $active_ctx->fast_curie_map;
  4284. // check for partial matches of against `iri`, which means look until
  4285. // iri.length - 1, not full length
  4286. $max_partial_length = strlen($iri) - 1;
  4287. for(; $idx < $max_partial_length && isset($iri_map[$iri[$idx]]); ++$idx) {
  4288. $iri_map = $iri_map[$iri[$idx]];
  4289. if(isset($iri_map[''])) {
  4290. $entry = $iri_map[''][0];
  4291. $entry->iri_length = $idx + 1;
  4292. $partial_matches[] = $entry;
  4293. }
  4294. }
  4295. // check partial matches in reverse order to prefer longest ones first
  4296. $partial_matches = array_reverse($partial_matches);
  4297. foreach($partial_matches as $entry) {
  4298. $terms = $entry->terms;
  4299. foreach($terms as $term) {
  4300. // a CURIE is usable if:
  4301. // 1. it has no mapping, OR
  4302. // 2. value is null, which means we're not compacting an @value, AND
  4303. // the mapping matches the IRI
  4304. $curie = $term . ':' . substr($iri, $entry->iri_length);
  4305. $is_usable_curie = (!property_exists($active_ctx->mappings, $curie) ||
  4306. ($value === null &&
  4307. $active_ctx->mappings->{$curie}->{'@id'} === $iri));
  4308. // select curie if it is shorter or the same length but
  4309. // lexicographically less than the current choice
  4310. if($is_usable_curie && ($choice === null ||
  4311. self::_compareShortestLeast($curie, $choice) < 0)) {
  4312. $choice = $curie;
  4313. }
  4314. }
  4315. }
  4316. // return chosen curie
  4317. if($choice !== null) {
  4318. return $choice;
  4319. }
  4320. // compact IRI relative to base
  4321. if(!$relative_to['vocab']) {
  4322. return jsonld_remove_base($active_ctx->{'@base'}, $iri);
  4323. }
  4324. // return IRI as is
  4325. return $iri;
  4326. }
  4327. /**
  4328. * Performs value compaction on an object with '@value' or '@id' as the only
  4329. * property.
  4330. *
  4331. * @param stdClass $active_ctx the active context.
  4332. * @param string $active_property the active property that points to the
  4333. * value.
  4334. * @param mixed $value the value to compact.
  4335. *
  4336. * @return mixed the compaction result.
  4337. */
  4338. protected function _compactValue($active_ctx, $active_property, $value) {
  4339. // value is a @value
  4340. if(self::_isValue($value)) {
  4341. // get context rules
  4342. $type = self::getContextValue($active_ctx, $active_property, '@type');
  4343. $language = self::getContextValue(
  4344. $active_ctx, $active_property, '@language');
  4345. $container = self::getContextValue(
  4346. $active_ctx, $active_property, '@container');
  4347. // whether or not the value has an @index that must be preserved
  4348. $preserve_index = (property_exists($value, '@index') &&
  4349. $container !== '@index');
  4350. // if there's no @index to preserve
  4351. if(!$preserve_index) {
  4352. // matching @type or @language specified in context, compact value
  4353. if(self::_hasKeyValue($value, '@type', $type) ||
  4354. self::_hasKeyValue($value, '@language', $language)) {
  4355. return $value->{'@value'};
  4356. }
  4357. }
  4358. // return just the value of @value if all are true:
  4359. // 1. @value is the only key or @index isn't being preserved
  4360. // 2. there is no default language or @value is not a string or
  4361. // the key has a mapping with a null @language
  4362. $key_count = count(array_keys((array)$value));
  4363. $is_value_only_key = ($key_count === 1 ||
  4364. ($key_count === 2 && property_exists($value, '@index') &&
  4365. !$preserve_index));
  4366. $has_default_language = property_exists($active_ctx, '@language');
  4367. $is_value_string = is_string($value->{'@value'});
  4368. $has_null_mapping = (
  4369. property_exists($active_ctx->mappings, $active_property) &&
  4370. $active_ctx->mappings->{$active_property} !== null &&
  4371. self::_hasKeyValue(
  4372. $active_ctx->mappings->{$active_property}, '@language', null));
  4373. if($is_value_only_key &&
  4374. (!$has_default_language || !$is_value_string || $has_null_mapping)) {
  4375. return $value->{'@value'};
  4376. }
  4377. $rval = new stdClass();
  4378. // preserve @index
  4379. if($preserve_index) {
  4380. $rval->{$this->_compactIri($active_ctx, '@index')} = $value->{'@index'};
  4381. }
  4382. // compact @type IRI
  4383. if(property_exists($value, '@type')) {
  4384. $rval->{$this->_compactIri($active_ctx, '@type')} = $this->_compactIri(
  4385. $active_ctx, $value->{'@type'}, null, array('vocab' => true));
  4386. } else if(property_exists($value, '@language')) {
  4387. // alias @language
  4388. $rval->{$this->_compactIri($active_ctx, '@language')} =
  4389. $value->{'@language'};
  4390. }
  4391. // alias @value
  4392. $rval->{$this->_compactIri($active_ctx, '@value')} = $value->{'@value'};
  4393. return $rval;
  4394. }
  4395. // value is a subject reference
  4396. $expanded_property = $this->_expandIri(
  4397. $active_ctx, $active_property, array('vocab' => true));
  4398. $type = self::getContextValue($active_ctx, $active_property, '@type');
  4399. $compacted = $this->_compactIri(
  4400. $active_ctx, $value->{'@id'}, null,
  4401. array('vocab' => ($type === '@vocab')));
  4402. // compact to scalar
  4403. if($type === '@id' || $type === '@vocab' ||
  4404. $expanded_property === '@graph') {
  4405. return $compacted;
  4406. }
  4407. $rval = (object)array(
  4408. $this->_compactIri($active_ctx, '@id') => $compacted);
  4409. return $rval;
  4410. }
  4411. /**
  4412. * Creates a term definition during context processing.
  4413. *
  4414. * @param stdClass $active_ctx the current active context.
  4415. * @param stdClass $local_ctx the local context being processed.
  4416. * @param string $term the key in the local context to define the mapping for.
  4417. * @param stdClass $defined a map of defining/defined keys to detect cycles
  4418. * and prevent double definitions.
  4419. */
  4420. protected function _createTermDefinition(
  4421. $active_ctx, $local_ctx, $term, $defined) {
  4422. if(property_exists($defined, $term)) {
  4423. // term already defined
  4424. if($defined->{$term}) {
  4425. return;
  4426. }
  4427. // cycle detected
  4428. throw new JsonLdException(
  4429. 'Cyclical context definition detected.',
  4430. 'jsonld.CyclicalContext', 'cyclic IRI mapping',
  4431. array('context' => $local_ctx, 'term' => $term));
  4432. }
  4433. // now defining term
  4434. $defined->{$term} = false;
  4435. if(self::_isKeyword($term)) {
  4436. throw new JsonLdException(
  4437. 'Invalid JSON-LD syntax; keywords cannot be overridden.',
  4438. 'jsonld.SyntaxError', 'keyword redefinition',
  4439. array('context' => $local_ctx, 'term' => $term));
  4440. }
  4441. // remove old mapping
  4442. if(property_exists($active_ctx->mappings, $term)) {
  4443. unset($active_ctx->mappings->{$term});
  4444. }
  4445. // get context term value
  4446. $value = $local_ctx->{$term};
  4447. // clear context entry
  4448. if($value === null || (is_object($value) &&
  4449. self::_hasKeyValue($value, '@id', null))) {
  4450. $active_ctx->mappings->{$term} = null;
  4451. $defined->{$term} = true;
  4452. return;
  4453. }
  4454. // convert short-hand value to object w/@id
  4455. if(is_string($value)) {
  4456. $value = (object)array('@id' => $value);
  4457. }
  4458. if(!is_object($value)) {
  4459. throw new JsonLdException(
  4460. 'Invalid JSON-LD syntax; @context property values must be ' .
  4461. 'strings or objects.', 'jsonld.SyntaxError', 'invalid term definition',
  4462. array('context' => $local_ctx));
  4463. }
  4464. // create new mapping
  4465. $mapping = $active_ctx->mappings->{$term} = new stdClass();
  4466. $mapping->reverse = false;
  4467. if(property_exists($value, '@reverse')) {
  4468. if(property_exists($value, '@id')) {
  4469. throw new JsonLdException(
  4470. 'Invalid JSON-LD syntax; a @reverse term definition must not ' +
  4471. 'contain @id.', 'jsonld.SyntaxError', 'invalid reverse property',
  4472. array('context' => $local_ctx));
  4473. }
  4474. $reverse = $value->{'@reverse'};
  4475. if(!is_string($reverse)) {
  4476. throw new JsonLdException(
  4477. 'Invalid JSON-LD syntax; a @context @reverse value must be a string.',
  4478. 'jsonld.SyntaxError', 'invalid IRI mapping',
  4479. array('context' => $local_ctx));
  4480. }
  4481. // expand and add @id mapping
  4482. $id = $this->_expandIri(
  4483. $active_ctx, $reverse, array('vocab' => true, 'base' => false),
  4484. $local_ctx, $defined);
  4485. if(!self::_isAbsoluteIri($id)) {
  4486. throw new JsonLdException(
  4487. 'Invalid JSON-LD syntax; @context @reverse value must be ' .
  4488. 'an absolute IRI or a blank node identifier.',
  4489. 'jsonld.SyntaxError', 'invalid IRI mapping',
  4490. array('context' => $local_ctx));
  4491. }
  4492. $mapping->{'@id'} = $id;
  4493. $mapping->reverse = true;
  4494. } else if(property_exists($value, '@id')) {
  4495. $id = $value->{'@id'};
  4496. if(!is_string($id)) {
  4497. throw new JsonLdException(
  4498. 'Invalid JSON-LD syntax; @context @id value must be a string.',
  4499. 'jsonld.SyntaxError', 'invalid IRI mapping',
  4500. array('context' => $local_ctx));
  4501. }
  4502. if($id !== $term) {
  4503. // add @id to mapping
  4504. $id = $this->_expandIri(
  4505. $active_ctx, $id, array('vocab' => true, 'base' => false),
  4506. $local_ctx, $defined);
  4507. if(!self::_isAbsoluteIri($id) && !self::_isKeyword($id)) {
  4508. throw new JsonLdException(
  4509. 'Invalid JSON-LD syntax; @context @id value must be an ' .
  4510. 'absolute IRI, a blank node identifier, or a keyword.',
  4511. 'jsonld.SyntaxError', 'invalid IRI mapping',
  4512. array('context' => $local_ctx));
  4513. }
  4514. $mapping->{'@id'} = $id;
  4515. }
  4516. }
  4517. // always compute whether term has a colon as an optimization for
  4518. // _compactIri
  4519. $colon = strpos($term, ':');
  4520. $mapping->_term_has_colon = ($colon !== false);
  4521. if(!property_exists($mapping, '@id')) {
  4522. // see if the term has a prefix
  4523. if($mapping->_term_has_colon) {
  4524. $prefix = substr($term, 0, $colon);
  4525. if(property_exists($local_ctx, $prefix)) {
  4526. // define parent prefix
  4527. $this->_createTermDefinition(
  4528. $active_ctx, $local_ctx, $prefix, $defined);
  4529. }
  4530. if(property_exists($active_ctx->mappings, $prefix) &&
  4531. $active_ctx->mappings->{$prefix}) {
  4532. // set @id based on prefix parent
  4533. $suffix = substr($term, $colon + 1);
  4534. $mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} .
  4535. $suffix;
  4536. } else {
  4537. // term is an absolute IRI
  4538. $mapping->{'@id'} = $term;
  4539. }
  4540. } else {
  4541. // non-IRIs *must* define @ids if @vocab is not available
  4542. if(!property_exists($active_ctx, '@vocab')) {
  4543. throw new JsonLdException(
  4544. 'Invalid JSON-LD syntax; @context terms must define an @id.',
  4545. 'jsonld.SyntaxError', 'invalid IRI mapping',
  4546. array('context' => $local_ctx, 'term' => $term));
  4547. }
  4548. // prepend vocab to term
  4549. $mapping->{'@id'} = $active_ctx->{'@vocab'} . $term;
  4550. }
  4551. }
  4552. // optimization to store length of @id once for _compactIri
  4553. $mapping->_id_length = strlen($mapping->{'@id'});
  4554. // IRI mapping now defined
  4555. $defined->{$term} = true;
  4556. if(property_exists($value, '@type')) {
  4557. $type = $value->{'@type'};
  4558. if(!is_string($type)) {
  4559. throw new JsonLdException(
  4560. 'Invalid JSON-LD syntax; @context @type values must be strings.',
  4561. 'jsonld.SyntaxError', 'invalid type mapping',
  4562. array('context' => $local_ctx));
  4563. }
  4564. if($type !== '@id' && $type !== '@vocab') {
  4565. // expand @type to full IRI
  4566. $type = $this->_expandIri(
  4567. $active_ctx, $type, array('vocab' => true), $local_ctx, $defined);
  4568. if(!self::_isAbsoluteIri($type)) {
  4569. throw new JsonLdException(
  4570. 'Invalid JSON-LD syntax; an @context @type value must ' .
  4571. 'be an absolute IRI.', 'jsonld.SyntaxError',
  4572. 'invalid type mapping', array('context' => $local_ctx));
  4573. }
  4574. if(strpos($type, '_:') === 0) {
  4575. throw new JsonLdException(
  4576. 'Invalid JSON-LD syntax; an @context @type values must ' .
  4577. 'be an IRI, not a blank node identifier.',
  4578. 'jsonld.SyntaxError', 'invalid type mapping',
  4579. array('context' => $local_ctx));
  4580. }
  4581. }
  4582. // add @type to mapping
  4583. $mapping->{'@type'} = $type;
  4584. }
  4585. if(property_exists($value, '@container')) {
  4586. $container = $value->{'@container'};
  4587. if($container !== '@list' && $container !== '@set' &&
  4588. $container !== '@index' && $container !== '@language') {
  4589. throw new JsonLdException(
  4590. 'Invalid JSON-LD syntax; @context @container value must be ' .
  4591. 'one of the following: @list, @set, @index, or @language.',
  4592. 'jsonld.SyntaxError', 'invalid container mapping',
  4593. array('context' => $local_ctx));
  4594. }
  4595. if($mapping->reverse && $container !== '@index' &&
  4596. $container !== '@set' && $container !== null) {
  4597. throw new JsonLdException(
  4598. 'Invalid JSON-LD syntax; @context @container value for a @reverse ' +
  4599. 'type definition must be @index or @set.',
  4600. 'jsonld.SyntaxError', 'invalid reverse property',
  4601. array('context' => $local_ctx));
  4602. }
  4603. // add @container to mapping
  4604. $mapping->{'@container'} = $container;
  4605. }
  4606. if(property_exists($value, '@language') &&
  4607. !property_exists($value, '@type')) {
  4608. $language = $value->{'@language'};
  4609. if($language !== null && !is_string($language)) {
  4610. throw new JsonLdException(
  4611. 'Invalid JSON-LD syntax; @context @language value must be ' .
  4612. 'a string or null.', 'jsonld.SyntaxError',
  4613. 'invalid language mapping', array('context' => $local_ctx));
  4614. }
  4615. // add @language to mapping
  4616. if($language !== null) {
  4617. $language = strtolower($language);
  4618. }
  4619. $mapping->{'@language'} = $language;
  4620. }
  4621. // disallow aliasing @context and @preserve
  4622. $id = $mapping->{'@id'};
  4623. if($id === '@context' || $id === '@preserve') {
  4624. throw new JsonLdException(
  4625. 'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.',
  4626. 'jsonld.SyntaxError', 'invalid keyword alias',
  4627. array('context' => $local_ctx));
  4628. }
  4629. }
  4630. /**
  4631. * Expands a string to a full IRI. The string may be a term, a prefix, a
  4632. * relative IRI, or an absolute IRI. The associated absolute IRI will be
  4633. * returned.
  4634. *
  4635. * @param stdClass $active_ctx the current active context.
  4636. * @param string $value the string to expand.
  4637. * @param assoc $relative_to options for how to resolve relative IRIs:
  4638. * base: true to resolve against the base IRI, false not to.
  4639. * vocab: true to concatenate after @vocab, false not to.
  4640. * @param stdClass $local_ctx the local context being processed (only given
  4641. * if called during document processing).
  4642. * @param defined a map for tracking cycles in context definitions (only given
  4643. * if called during document processing).
  4644. *
  4645. * @return mixed the expanded value.
  4646. */
  4647. function _expandIri(
  4648. $active_ctx, $value, $relative_to=array(), $local_ctx=null, $defined=null) {
  4649. // already expanded
  4650. if($value === null || self::_isKeyword($value)) {
  4651. return $value;
  4652. }
  4653. // define term dependency if not defined
  4654. if($local_ctx !== null && property_exists($local_ctx, $value) &&
  4655. !self::_hasKeyValue($defined, $value, true)) {
  4656. $this->_createTermDefinition($active_ctx, $local_ctx, $value, $defined);
  4657. }
  4658. if(isset($relative_to['vocab']) && $relative_to['vocab']) {
  4659. if(property_exists($active_ctx->mappings, $value)) {
  4660. $mapping = $active_ctx->mappings->{$value};
  4661. // value is explicitly ignored with a null mapping
  4662. if($mapping === null) {
  4663. return null;
  4664. }
  4665. // value is a term
  4666. return $mapping->{'@id'};
  4667. }
  4668. }
  4669. // split value into prefix:suffix
  4670. $colon = strpos($value, ':');
  4671. if($colon !== false) {
  4672. $prefix = substr($value, 0, $colon);
  4673. $suffix = substr($value, $colon + 1);
  4674. // do not expand blank nodes (prefix of '_') or already-absolute
  4675. // IRIs (suffix of '//')
  4676. if($prefix === '_' || strpos($suffix, '//') === 0) {
  4677. return $value;
  4678. }
  4679. // prefix dependency not defined, define it
  4680. if($local_ctx !== null && property_exists($local_ctx, $prefix)) {
  4681. $this->_createTermDefinition(
  4682. $active_ctx, $local_ctx, $prefix, $defined);
  4683. }
  4684. // use mapping if prefix is defined
  4685. if(property_exists($active_ctx->mappings, $prefix)) {
  4686. $mapping = $active_ctx->mappings->{$prefix};
  4687. if($mapping) {
  4688. return $mapping->{'@id'} . $suffix;
  4689. }
  4690. }
  4691. // already absolute IRI
  4692. return $value;
  4693. }
  4694. // prepend vocab
  4695. if(isset($relative_to['vocab']) && $relative_to['vocab'] &&
  4696. property_exists($active_ctx, '@vocab')) {
  4697. return $active_ctx->{'@vocab'} . $value;
  4698. }
  4699. // prepend base
  4700. $rval = $value;
  4701. if(isset($relative_to['base']) && $relative_to['base']) {
  4702. $rval = jsonld_prepend_base($active_ctx->{'@base'}, $rval);
  4703. }
  4704. return $rval;
  4705. }
  4706. /**
  4707. * Finds all @context URLs in the given JSON-LD input.
  4708. *
  4709. * @param mixed $input the JSON-LD input.
  4710. * @param stdClass $urls a map of URLs (url => false/@contexts).
  4711. * @param bool $replace true to replace the URLs in the given input with
  4712. * the @contexts from the urls map, false not to.
  4713. * @param string $base the base URL to resolve relative URLs with.
  4714. */
  4715. protected function _findContextUrls($input, $urls, $replace, $base) {
  4716. if(is_array($input)) {
  4717. foreach($input as $e) {
  4718. $this->_findContextUrls($e, $urls, $replace, $base);
  4719. }
  4720. } else if(is_object($input)) {
  4721. foreach($input as $k => &$v) {
  4722. if($k !== '@context') {
  4723. $this->_findContextUrls($v, $urls, $replace, $base);
  4724. continue;
  4725. }
  4726. // array @context
  4727. if(is_array($v)) {
  4728. $length = count($v);
  4729. for($i = 0; $i < $length; ++$i) {
  4730. if(is_string($v[$i])) {
  4731. $url = jsonld_prepend_base($base, $v[$i]);
  4732. // replace w/@context if requested
  4733. if($replace) {
  4734. $ctx = $urls->{$url};
  4735. if(is_array($ctx)) {
  4736. // add flattened context
  4737. array_splice($v, $i, 1, $ctx);
  4738. $i += count($ctx) - 1;
  4739. $length = count($v);
  4740. } else {
  4741. $v[$i] = $ctx;
  4742. }
  4743. } else if(!property_exists($urls, $url)) {
  4744. // @context URL found
  4745. $urls->{$url} = false;
  4746. }
  4747. }
  4748. }
  4749. } else if(is_string($v)) {
  4750. // string @context
  4751. $v = jsonld_prepend_base($base, $v);
  4752. // replace w/@context if requested
  4753. if($replace) {
  4754. $input->{$k} = $urls->{$v};
  4755. } else if(!property_exists($urls, $v)) {
  4756. // @context URL found
  4757. $urls->{$v} = false;
  4758. }
  4759. }
  4760. }
  4761. }
  4762. }
  4763. /**
  4764. * Retrieves external @context URLs using the given document loader. Each
  4765. * instance of @context in the input that refers to a URL will be replaced
  4766. * with the JSON @context found at that URL.
  4767. *
  4768. * @param mixed $input the JSON-LD input with possible contexts.
  4769. * @param stdClass $cycles an object for tracking context cycles.
  4770. * @param callable $load_document(url) the document loader.
  4771. * @param base $base the base URL to resolve relative URLs against.
  4772. *
  4773. * @return mixed the result.
  4774. */
  4775. protected function _retrieveContextUrls(
  4776. &$input, $cycles, $load_document, $base='') {
  4777. if(count(get_object_vars($cycles)) > self::MAX_CONTEXT_URLS) {
  4778. throw new JsonLdException(
  4779. 'Maximum number of @context URLs exceeded.',
  4780. 'jsonld.ContextUrlError', 'loading remote context failed',
  4781. array('max' => self::MAX_CONTEXT_URLS));
  4782. }
  4783. // for tracking the URLs to retrieve
  4784. $urls = new stdClass();
  4785. // regex for validating URLs
  4786. $regex = '/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/';
  4787. // find all URLs in the given input
  4788. $this->_findContextUrls($input, $urls, false, $base);
  4789. // queue all unretrieved URLs
  4790. $queue = array();
  4791. foreach($urls as $url => $ctx) {
  4792. if($ctx === false) {
  4793. // validate URL
  4794. if(!preg_match($regex, $url)) {
  4795. throw new JsonLdException(
  4796. 'Malformed or unsupported URL.', 'jsonld.InvalidUrl',
  4797. 'loading remote context failed', array('url' => $url));
  4798. }
  4799. $queue[] = $url;
  4800. }
  4801. }
  4802. // retrieve URLs in queue
  4803. foreach($queue as $url) {
  4804. // check for context URL cycle
  4805. if(property_exists($cycles, $url)) {
  4806. throw new JsonLdException(
  4807. 'Cyclical @context URLs detected.',
  4808. 'jsonld.ContextUrlError', 'recursive context inclusion',
  4809. array('url' => $url));
  4810. }
  4811. $_cycles = self::copy($cycles);
  4812. $_cycles->{$url} = true;
  4813. // retrieve URL
  4814. $remote_doc = call_user_func($load_document, $url);
  4815. $ctx = $remote_doc->document;
  4816. // parse string context as JSON
  4817. if(is_string($ctx)) {
  4818. try {
  4819. $ctx = self::_parse_json($ctx);
  4820. } catch(Exception $e) {
  4821. throw new JsonLdException(
  4822. 'Could not parse JSON from URL.',
  4823. 'jsonld.ParseError', 'loading remote context failed',
  4824. array('url' => $url), $e);
  4825. }
  4826. }
  4827. // ensure ctx is an object
  4828. if(!is_object($ctx)) {
  4829. throw new JsonLdException(
  4830. 'Derefencing a URL did not result in a valid JSON-LD object.',
  4831. 'jsonld.InvalidUrl', 'invalid remote context', array('url' => $url));
  4832. }
  4833. // use empty context if no @context key is present
  4834. if(!property_exists($ctx, '@context')) {
  4835. $ctx = (object)array('@context' => new stdClass());
  4836. } else {
  4837. $ctx = (object)array('@context' => $ctx->{'@context'});
  4838. }
  4839. // append context URL to context if given
  4840. if($remote_doc->contextUrl !== null) {
  4841. $ctx->{'@context'} = self::arrayify($ctx->{'@context'});
  4842. $ctx->{'@context'}[] = $remote_doc->contextUrl;
  4843. }
  4844. // recurse
  4845. $this->_retrieveContextUrls($ctx, $_cycles, $load_document, $url);
  4846. $urls->{$url} = $ctx->{'@context'};
  4847. }
  4848. // replace all URLS in the input
  4849. $this->_findContextUrls($input, $urls, true, $base);
  4850. }
  4851. /**
  4852. * Gets the initial context.
  4853. *
  4854. * @param assoc $options the options to use.
  4855. * base the document base IRI.
  4856. *
  4857. * @return stdClass the initial context.
  4858. */
  4859. protected function _getInitialContext($options) {
  4860. return (object)array(
  4861. '@base' => jsonld_parse_url($options['base']),
  4862. 'mappings' => new stdClass(),
  4863. 'inverse' => null);
  4864. }
  4865. /**
  4866. * Generates an inverse context for use in the compaction algorithm, if
  4867. * not already generated for the given active context.
  4868. *
  4869. * @param stdClass $active_ctx the active context to use.
  4870. *
  4871. * @return stdClass the inverse context.
  4872. */
  4873. protected function _getInverseContext($active_ctx) {
  4874. // inverse context already generated
  4875. if($active_ctx->inverse) {
  4876. return $active_ctx->inverse;
  4877. }
  4878. $inverse = $active_ctx->inverse = new stdClass();
  4879. // variables for building fast CURIE map
  4880. $fast_curie_map = $active_ctx->fast_curie_map = new ArrayObject();
  4881. $iris_to_terms = array();
  4882. // handle default language
  4883. $default_language = '@none';
  4884. if(property_exists($active_ctx, '@language')) {
  4885. $default_language = $active_ctx->{'@language'};
  4886. }
  4887. // create term selections for each mapping in the context, ordered by
  4888. // shortest and then lexicographically least
  4889. $mappings = $active_ctx->mappings;
  4890. $terms = array_keys((array)$mappings);
  4891. usort($terms, array($this, '_compareShortestLeast'));
  4892. foreach($terms as $term) {
  4893. $mapping = $mappings->{$term};
  4894. if($mapping === null) {
  4895. continue;
  4896. }
  4897. // add term selection where it applies
  4898. if(property_exists($mapping, '@container')) {
  4899. $container = $mapping->{'@container'};
  4900. } else {
  4901. $container = '@none';
  4902. }
  4903. // iterate over every IRI in the mapping
  4904. $iris = $mapping->{'@id'};
  4905. $iris = self::arrayify($iris);
  4906. foreach($iris as $iri) {
  4907. $is_keyword = self::_isKeyword($iri);
  4908. // initialize container map
  4909. if(!property_exists($inverse, $iri)) {
  4910. $inverse->{$iri} = new stdClass();
  4911. if(!$is_keyword && !$mapping->_term_has_colon) {
  4912. // init IRI to term map and fast CURIE map
  4913. $iris_to_terms[$iri] = new ArrayObject();
  4914. $iris_to_terms[$iri][] = $term;
  4915. $fast_curie_entry = (object)array(
  4916. 'iri' => $iri, 'terms' => $iris_to_terms[$iri]);
  4917. if(!array_key_exists($iri[0], (array)$fast_curie_map)) {
  4918. $fast_curie_map[$iri[0]] = new ArrayObject();
  4919. }
  4920. $fast_curie_map[$iri[0]][] = $fast_curie_entry;
  4921. }
  4922. } else if(!$is_keyword && !$mapping->_term_has_colon) {
  4923. // add IRI to term match
  4924. $iris_to_terms[$iri][] = $term;
  4925. }
  4926. $container_map = $inverse->{$iri};
  4927. // add new entry
  4928. if(!property_exists($container_map, $container)) {
  4929. $container_map->{$container} = (object)array(
  4930. '@language' => new stdClass(),
  4931. '@type' => new stdClass());
  4932. }
  4933. $entry = $container_map->{$container};
  4934. if($mapping->reverse) {
  4935. // term is preferred for values using @reverse
  4936. $this->_addPreferredTerm(
  4937. $mapping, $term, $entry->{'@type'}, '@reverse');
  4938. } else if(property_exists($mapping, '@type')) {
  4939. // term is preferred for values using specific type
  4940. $this->_addPreferredTerm(
  4941. $mapping, $term, $entry->{'@type'}, $mapping->{'@type'});
  4942. } else if(property_exists($mapping, '@language')) {
  4943. // term is preferred for values using specific language
  4944. $language = $mapping->{'@language'};
  4945. if($language === null) {
  4946. $language = '@null';
  4947. }
  4948. $this->_addPreferredTerm(
  4949. $mapping, $term, $entry->{'@language'}, $language);
  4950. } else {
  4951. // term is preferred for values w/default language or no type and
  4952. // no language
  4953. // add an entry for the default language
  4954. $this->_addPreferredTerm(
  4955. $mapping, $term, $entry->{'@language'}, $default_language);
  4956. // add entries for no type and no language
  4957. $this->_addPreferredTerm(
  4958. $mapping, $term, $entry->{'@type'}, '@none');
  4959. $this->_addPreferredTerm(
  4960. $mapping, $term, $entry->{'@language'}, '@none');
  4961. }
  4962. }
  4963. }
  4964. // build fast CURIE map
  4965. foreach($fast_curie_map as $key => $value) {
  4966. $this->_buildIriMap($fast_curie_map, $key, 1);
  4967. }
  4968. return $inverse;
  4969. }
  4970. /**
  4971. * Runs a recursive algorithm to build a lookup map for quickly finding
  4972. * potential CURIEs.
  4973. *
  4974. * @param ArrayObject $iri_map the map to build.
  4975. * @param string $key the current key in the map to work on.
  4976. * @param int $idx the index into the IRI to compare.
  4977. */
  4978. function _buildIriMap($iri_map, $key, $idx) {
  4979. $entries = $iri_map[$key];
  4980. $next = $iri_map[$key] = new ArrayObject();
  4981. foreach($entries as $entry) {
  4982. $iri = $entry->iri;
  4983. if($idx >= strlen($iri)) {
  4984. $letter = '';
  4985. } else {
  4986. $letter = $iri[$idx];
  4987. }
  4988. if(!isset($next[$letter])) {
  4989. $next[$letter] = new ArrayObject();
  4990. }
  4991. $next[$letter][] = $entry;
  4992. }
  4993. foreach($next as $key => $value) {
  4994. if($key === '') {
  4995. continue;
  4996. }
  4997. $this->_buildIriMap($next, $key, $idx + 1);
  4998. }
  4999. }
  5000. /**
  5001. * Adds the term for the given entry if not already added.
  5002. *
  5003. * @param stdClass $mapping the term mapping.
  5004. * @param string $term the term to add.
  5005. * @param stdClass $entry the inverse context type_or_language entry to
  5006. * add to.
  5007. * @param string $type_or_language_value the key in the entry to add to.
  5008. */
  5009. function _addPreferredTerm($mapping, $term, $entry, $type_or_language_value) {
  5010. if(!property_exists($entry, $type_or_language_value)) {
  5011. $entry->{$type_or_language_value} = $term;
  5012. }
  5013. }
  5014. /**
  5015. * Clones an active context, creating a child active context.
  5016. *
  5017. * @return stdClass a clone (child) of the active context.
  5018. */
  5019. protected function _cloneActiveContext($active_ctx) {
  5020. $child = new stdClass();
  5021. $child->{'@base'} = $active_ctx->{'@base'};
  5022. $child->mappings = self::copy($active_ctx->mappings);
  5023. $child->inverse = null;
  5024. if(property_exists($active_ctx, '@language')) {
  5025. $child->{'@language'} = $active_ctx->{'@language'};
  5026. }
  5027. if(property_exists($active_ctx, '@vocab')) {
  5028. $child->{'@vocab'} = $active_ctx->{'@vocab'};
  5029. }
  5030. return $child;
  5031. }
  5032. /**
  5033. * Returns whether or not the given value is a keyword.
  5034. *
  5035. * @param string $v the value to check.
  5036. *
  5037. * @return bool true if the value is a keyword, false if not.
  5038. */
  5039. protected static function _isKeyword($v) {
  5040. if(!is_string($v)) {
  5041. return false;
  5042. }
  5043. switch($v) {
  5044. case '@base':
  5045. case '@context':
  5046. case '@container':
  5047. case '@default':
  5048. case '@embed':
  5049. case '@explicit':
  5050. case '@graph':
  5051. case '@id':
  5052. case '@index':
  5053. case '@language':
  5054. case '@list':
  5055. case '@omitDefault':
  5056. case '@preserve':
  5057. case '@requireAll':
  5058. case '@reverse':
  5059. case '@set':
  5060. case '@type':
  5061. case '@value':
  5062. case '@vocab':
  5063. return true;
  5064. }
  5065. return false;
  5066. }
  5067. /**
  5068. * Returns true if the given value is an empty Object.
  5069. *
  5070. * @param mixed $v the value to check.
  5071. *
  5072. * @return bool true if the value is an empty Object, false if not.
  5073. */
  5074. protected static function _isEmptyObject($v) {
  5075. return is_object($v) && count(get_object_vars($v)) === 0;
  5076. }
  5077. /**
  5078. * Throws an exception if the given value is not a valid @type value.
  5079. *
  5080. * @param mixed $v the value to check.
  5081. */
  5082. protected static function _validateTypeValue($v) {
  5083. // must be a string or empty object
  5084. if(is_string($v) || self::_isEmptyObject($v)) {
  5085. return;
  5086. }
  5087. // must be an array
  5088. $is_valid = false;
  5089. if(is_array($v)) {
  5090. // must contain only strings
  5091. $is_valid = true;
  5092. foreach($v as $e) {
  5093. if(!(is_string($e))) {
  5094. $is_valid = false;
  5095. break;
  5096. }
  5097. }
  5098. }
  5099. if(!$is_valid) {
  5100. throw new JsonLdException(
  5101. 'Invalid JSON-LD syntax; "@type" value must a string, an array ' .
  5102. 'of strings, or an empty object.',
  5103. 'jsonld.SyntaxError', 'invalid type value', array('value' => $v));
  5104. }
  5105. }
  5106. /**
  5107. * Returns true if the given value is a subject with properties.
  5108. *
  5109. * @param mixed $v the value to check.
  5110. *
  5111. * @return bool true if the value is a subject with properties, false if not.
  5112. */
  5113. protected static function _isSubject($v) {
  5114. // Note: A value is a subject if all of these hold true:
  5115. // 1. It is an Object.
  5116. // 2. It is not a @value, @set, or @list.
  5117. // 3. It has more than 1 key OR any existing key is not @id.
  5118. $rval = false;
  5119. if(is_object($v) &&
  5120. !property_exists($v, '@value') &&
  5121. !property_exists($v, '@set') &&
  5122. !property_exists($v, '@list')) {
  5123. $count = count(get_object_vars($v));
  5124. $rval = ($count > 1 || !property_exists($v, '@id'));
  5125. }
  5126. return $rval;
  5127. }
  5128. /**
  5129. * Returns true if the given value is a subject reference.
  5130. *
  5131. * @param mixed $v the value to check.
  5132. *
  5133. * @return bool true if the value is a subject reference, false if not.
  5134. */
  5135. protected static function _isSubjectReference($v) {
  5136. // Note: A value is a subject reference if all of these hold true:
  5137. // 1. It is an Object.
  5138. // 2. It has a single key: @id.
  5139. return (is_object($v) && count(get_object_vars($v)) === 1 &&
  5140. property_exists($v, '@id'));
  5141. }
  5142. /**
  5143. * Returns true if the given value is a @value.
  5144. *
  5145. * @param mixed $v the value to check.
  5146. *
  5147. * @return bool true if the value is a @value, false if not.
  5148. */
  5149. protected static function _isValue($v) {
  5150. // Note: A value is a @value if all of these hold true:
  5151. // 1. It is an Object.
  5152. // 2. It has the @value property.
  5153. return is_object($v) && property_exists($v, '@value');
  5154. }
  5155. /**
  5156. * Returns true if the given value is a @list.
  5157. *
  5158. * @param mixed $v the value to check.
  5159. *
  5160. * @return bool true if the value is a @list, false if not.
  5161. */
  5162. protected static function _isList($v) {
  5163. // Note: A value is a @list if all of these hold true:
  5164. // 1. It is an Object.
  5165. // 2. It has the @list property.
  5166. return is_object($v) && property_exists($v, '@list');
  5167. }
  5168. /**
  5169. * Returns true if the given value is a blank node.
  5170. *
  5171. * @param mixed $v the value to check.
  5172. *
  5173. * @return bool true if the value is a blank node, false if not.
  5174. */
  5175. protected static function _isBlankNode($v) {
  5176. // Note: A value is a blank node if all of these hold true:
  5177. // 1. It is an Object.
  5178. // 2. If it has an @id key its value begins with '_:'.
  5179. // 3. It has no keys OR is not a @value, @set, or @list.
  5180. $rval = false;
  5181. if(is_object($v)) {
  5182. if(property_exists($v, '@id')) {
  5183. $rval = (strpos($v->{'@id'}, '_:') === 0);
  5184. } else {
  5185. $rval = (count(get_object_vars($v)) === 0 ||
  5186. !(property_exists($v, '@value') ||
  5187. property_exists($v, '@set') ||
  5188. property_exists($v, '@list')));
  5189. }
  5190. }
  5191. return $rval;
  5192. }
  5193. /**
  5194. * Returns true if the given value is an absolute IRI, false if not.
  5195. *
  5196. * @param string $v the value to check.
  5197. *
  5198. * @return bool true if the value is an absolute IRI, false if not.
  5199. */
  5200. protected static function _isAbsoluteIri($v) {
  5201. return strpos($v, ':') !== false;
  5202. }
  5203. /**
  5204. * Returns true if the given target has the given key and its
  5205. * value equals is the given value.
  5206. *
  5207. * @param stdClass $target the target object.
  5208. * @param string key the key to check.
  5209. * @param mixed $value the value to check.
  5210. *
  5211. * @return bool true if the target has the given key and its value matches.
  5212. */
  5213. protected static function _hasKeyValue($target, $key, $value) {
  5214. return (property_exists($target, $key) && $target->{$key} === $value);
  5215. }
  5216. /**
  5217. * Returns true if both of the given objects have the same value for the
  5218. * given key or if neither of the objects contain the given key.
  5219. *
  5220. * @param stdClass $o1 the first object.
  5221. * @param stdClass $o2 the second object.
  5222. * @param string key the key to check.
  5223. *
  5224. * @return bool true if both objects have the same value for the key or
  5225. * neither has the key.
  5226. */
  5227. protected static function _compareKeyValues($o1, $o2, $key) {
  5228. if(property_exists($o1, $key)) {
  5229. return property_exists($o2, $key) && $o1->{$key} === $o2->{$key};
  5230. }
  5231. return !property_exists($o2, $key);
  5232. }
  5233. /**
  5234. * Parses JSON and sets an appropriate exception message on error.
  5235. *
  5236. * @param string $json the JSON to parse.
  5237. *
  5238. * @return mixed the parsed JSON object or array.
  5239. */
  5240. protected static function _parse_json($json) {
  5241. $rval = json_decode($json);
  5242. $error = json_last_error();
  5243. if($error === JSON_ERROR_NONE && $rval === null) {
  5244. $error = JSON_ERROR_SYNTAX;
  5245. }
  5246. switch($error) {
  5247. case JSON_ERROR_NONE:
  5248. break;
  5249. case JSON_ERROR_DEPTH:
  5250. throw new JsonLdException(
  5251. 'Could not parse JSON; the maximum stack depth has been exceeded.',
  5252. 'jsonld.ParseError');
  5253. case JSON_ERROR_STATE_MISMATCH:
  5254. throw new JsonLdException(
  5255. 'Could not parse JSON; invalid or malformed JSON.',
  5256. 'jsonld.ParseError');
  5257. case JSON_ERROR_CTRL_CHAR:
  5258. case JSON_ERROR_SYNTAX:
  5259. throw new JsonLdException(
  5260. 'Could not parse JSON; syntax error, malformed JSON.',
  5261. 'jsonld.ParseError');
  5262. case JSON_ERROR_UTF8:
  5263. throw new JsonLdException(
  5264. 'Could not parse JSON from URL; malformed UTF-8 characters.',
  5265. 'jsonld.ParseError');
  5266. default:
  5267. throw new JsonLdException(
  5268. 'Could not parse JSON from URL; unknown error.',
  5269. 'jsonld.ParseError');
  5270. }
  5271. return $rval;
  5272. }
  5273. }
  5274. // register the N-Quads RDF parser
  5275. jsonld_register_rdf_parser(
  5276. 'application/nquads', array('JsonLdProcessor', 'parseNQuads'));
  5277. /**
  5278. * A JSON-LD Exception.
  5279. */
  5280. class JsonLdException extends Exception {
  5281. public function __construct(
  5282. $msg, $type, $code='error', $details=null, $previous=null) {
  5283. $this->type = $type;
  5284. $this->code = $code;
  5285. $this->details = $details;
  5286. $this->cause = $previous;
  5287. parent::__construct($msg, 0, $previous);
  5288. }
  5289. public function __toString() {
  5290. $rval = __CLASS__ . ": [{$this->type}]: {$this->message}\n";
  5291. if($this->code) {
  5292. $rval .= 'Code: ' . $this->code . "\n";
  5293. }
  5294. if($this->details) {
  5295. $rval .= 'Details: ' . print_r($this->details, true) . "\n";
  5296. }
  5297. if($this->cause) {
  5298. $rval .= 'Cause: ' . $this->cause;
  5299. }
  5300. $rval .= $this->getTraceAsString() . "\n";
  5301. return $rval;
  5302. }
  5303. };
  5304. /**
  5305. * A UniqueNamer issues unique names, keeping track of any previously issued
  5306. * names.
  5307. */
  5308. class UniqueNamer {
  5309. /**
  5310. * Constructs a new UniqueNamer.
  5311. *
  5312. * @param prefix the prefix to use ('<prefix><counter>').
  5313. */
  5314. public function __construct($prefix) {
  5315. $this->prefix = $prefix;
  5316. $this->counter = 0;
  5317. $this->existing = new stdClass();
  5318. $this->order = array();
  5319. }
  5320. /**
  5321. * Clones this UniqueNamer.
  5322. */
  5323. public function __clone() {
  5324. $this->existing = clone $this->existing;
  5325. }
  5326. /**
  5327. * Gets the new name for the given old name, where if no old name is given
  5328. * a new name will be generated.
  5329. *
  5330. * @param mixed [$old_name] the old name to get the new name for.
  5331. *
  5332. * @return string the new name.
  5333. */
  5334. public function getName($old_name=null) {
  5335. // return existing old name
  5336. if($old_name && property_exists($this->existing, $old_name)) {
  5337. return $this->existing->{$old_name};
  5338. }
  5339. // get next name
  5340. $name = $this->prefix . $this->counter;
  5341. $this->counter += 1;
  5342. // save mapping
  5343. if($old_name !== null) {
  5344. $this->existing->{$old_name} = $name;
  5345. $this->order[] = $old_name;
  5346. }
  5347. return $name;
  5348. }
  5349. /**
  5350. * Returns true if the given old name has already been assigned a new name.
  5351. *
  5352. * @param string $old_name the old name to check.
  5353. *
  5354. * @return true if the old name has been assigned a new name, false if not.
  5355. */
  5356. public function isNamed($old_name) {
  5357. return property_exists($this->existing, $old_name);
  5358. }
  5359. }
  5360. /**
  5361. * A Permutator iterates over all possible permutations of the given array
  5362. * of elements.
  5363. */
  5364. class Permutator {
  5365. /**
  5366. * Constructs a new Permutator.
  5367. *
  5368. * @param array $list the array of elements to iterate over.
  5369. */
  5370. public function __construct($list) {
  5371. // original array
  5372. $this->list = $list;
  5373. sort($this->list);
  5374. // indicates whether there are more permutations
  5375. $this->done = false;
  5376. // directional info for permutation algorithm
  5377. $this->left = new stdClass();
  5378. foreach($list as $v) {
  5379. $this->left->{$v} = true;
  5380. }
  5381. }
  5382. /**
  5383. * Returns true if there is another permutation.
  5384. *
  5385. * @return bool true if there is another permutation, false if not.
  5386. */
  5387. public function hasNext() {
  5388. return !$this->done;
  5389. }
  5390. /**
  5391. * Gets the next permutation. Call hasNext() to ensure there is another one
  5392. * first.
  5393. *
  5394. * @return array the next permutation.
  5395. */
  5396. public function next() {
  5397. // copy current permutation
  5398. $rval = $this->list;
  5399. /* Calculate the next permutation using the Steinhaus-Johnson-Trotter
  5400. permutation algorithm. */
  5401. // get largest mobile element k
  5402. // (mobile: element is greater than the one it is looking at)
  5403. $k = null;
  5404. $pos = 0;
  5405. $length = count($this->list);
  5406. for($i = 0; $i < $length; ++$i) {
  5407. $element = $this->list[$i];
  5408. $left = $this->left->{$element};
  5409. if(($k === null || $element > $k) &&
  5410. (($left && $i > 0 && $element > $this->list[$i - 1]) ||
  5411. (!$left && $i < ($length - 1) && $element > $this->list[$i + 1]))) {
  5412. $k = $element;
  5413. $pos = $i;
  5414. }
  5415. }
  5416. // no more permutations
  5417. if($k === null) {
  5418. $this->done = true;
  5419. } else {
  5420. // swap k and the element it is looking at
  5421. $swap = $this->left->{$k} ? $pos - 1 : $pos + 1;
  5422. $this->list[$pos] = $this->list[$swap];
  5423. $this->list[$swap] = $k;
  5424. // reverse the direction of all elements larger than k
  5425. for($i = 0; $i < $length; ++$i) {
  5426. if($this->list[$i] > $k) {
  5427. $this->left->{$this->list[$i]} = !$this->left->{$this->list[$i]};
  5428. }
  5429. }
  5430. }
  5431. return $rval;
  5432. }
  5433. }
  5434. /**
  5435. * An ActiveContextCache caches active contexts so they can be reused without
  5436. * the overhead of recomputing them.
  5437. */
  5438. class ActiveContextCache {
  5439. /**
  5440. * Constructs a new ActiveContextCache.
  5441. *
  5442. * @param int size the maximum size of the cache, defaults to 100.
  5443. */
  5444. public function __construct($size=100) {
  5445. $this->order = array();
  5446. $this->cache = new stdClass();
  5447. $this->size = $size;
  5448. }
  5449. /**
  5450. * Gets an active context from the cache based on the current active
  5451. * context and the new local context.
  5452. *
  5453. * @param stdClass $active_ctx the current active context.
  5454. * @param stdClass $local_ctx the new local context.
  5455. *
  5456. * @return mixed a shared copy of the cached active context or null.
  5457. */
  5458. public function get($active_ctx, $local_ctx) {
  5459. $key1 = serialize($active_ctx);
  5460. $key2 = serialize($local_ctx);
  5461. if(property_exists($this->cache, $key1)) {
  5462. $level1 = $this->cache->{$key1};
  5463. if(property_exists($level1, $key2)) {
  5464. return $level1->{$key2};
  5465. }
  5466. }
  5467. return null;
  5468. }
  5469. /**
  5470. * Sets an active context in the cache based on the previous active
  5471. * context and the just-processed local context.
  5472. *
  5473. * @param stdClass $active_ctx the previous active context.
  5474. * @param stdClass $local_ctx the just-processed local context.
  5475. * @param stdClass $result the resulting active context.
  5476. */
  5477. public function set($active_ctx, $local_ctx, $result) {
  5478. if(count($this->order) === $this->size) {
  5479. $entry = array_shift($this->order);
  5480. unset($this->cache->{$entry->activeCtx}->{$entry->localCtx});
  5481. }
  5482. $key1 = serialize($active_ctx);
  5483. $key2 = serialize($local_ctx);
  5484. $this->order[] = (object)array(
  5485. 'activeCtx' => $key1, 'localCtx' => $key2);
  5486. if(!property_exists($this->cache, $key1)) {
  5487. $this->cache->{$key1} = new stdClass();
  5488. }
  5489. $this->cache->{$key1}->{$key2} = JsonLdProcessor::copy($result);
  5490. }
  5491. }
  5492. /* end of file, omit ?> */