Compare commits

..

56 commits

Author SHA1 Message Date
ca3916d10d Fix extraneous parenthesis 2018-10-08 20:41:00 +00:00
f62652abff Release 1.1.0 2018-10-08 20:04:53 +00:00
ccbd3d6bc4 Switch from sha1 to sha256 2018-10-08 20:03:34 +00:00
7b779b2b53 Fix composer.json config keys 2018-09-27 21:04:23 -04:00
a9ac64daf0 Release 1.0 2018-09-27 20:01:12 -04:00
23cd99e8fb Update file formatting to PSR-2 (with tabs)
- Use short array syntax
- Bump required PHP version to PHP 5.4
- Update version to 1.0.0
2018-09-27 19:59:13 -04:00
Hypolite Petovan
56b01d779d Friendica fork of the php-json-ld repository
- Removed mentions of Travis
- Removed mentions of Digitalbazaar commercial support
2018-09-27 19:40:44 -04:00
Dave Longley
38b07bbe59 Start 0.4.8. 2016-04-25 00:18:09 -04:00
Dave Longley
dc1bd23f0e Release 0.4.7. 2016-04-25 00:17:52 -04:00
Dave Longley
3433a01a65 Add optimization for finding the best CURIE.
- Build a map for searching for a matching IRI when
  computing the inverse context. Each letter of an
  IRI can be used to key into the map to find the
  best set of partial matches (which can be used
  to create CURIEs).
- This approach is a faster alternative to trying each
  possible term in the active context as a possible
  CURIE, linearly, one at a time.
2016-04-25 00:16:38 -04:00
Dave Longley
956fb8b790 Add optimization for compacting keywords. 2016-04-24 17:01:42 -04:00
Dave Longley
81b072b438 Remove unnecessary compare. 2016-04-24 01:17:09 -04:00
Dave Longley
6b5fba05e5 Add optimization that measures term @id definition once. 2016-04-23 23:48:09 -04:00
Dave Longley
9759c9340d Add optimization that caches presence of colon in term. 2016-04-23 22:39:08 -04:00
Dave Longley
1abb809e8e Show how to specify URDNA2015 algorithm. 2016-01-05 10:11:47 -05:00
David I. Lehn
b16d43aa74 Disable normalization tests. 2015-10-13 14:32:40 -04:00
David I. Lehn
d832d72b09 Remove unneeded FIXME. 2015-10-13 14:09:42 -04:00
David I. Lehn
e98b8f61ba Group tests by test suite.
- Grouping the tests by test suite to avoid warning failures when there
  are no test in a suite. Requires the use of the phpunit `--group`
  flag.
2015-10-13 14:08:27 -04:00
David I. Lehn
c63a5961fb Add support for normalization test suite.
- Handle rdfn:Urgna2012EvalTest and rdfn:Urdna2015EvalTest.
- Use nquads input format for normalization tests.
- Support normalization test suite format with various tweaks.
  Compacting the data would eliminate most of these changes but can't
  rely on compact to work in the code that tests compact! So hard coded
  fixes are used.
  - Support 'entries' and 'sequence' for tests.
  - Support 'include' as filename without a .jsonld extension.
  - Support 'type' and '@type' as aliases.
  - Support 'id' and '@id' as aliases.
  - Support 'action' and 'input' as aliases.
  - Support 'result' and 'expect' as aliases.
  - No longer strip '#t' prefix from '#tNNN' ids.
  - Default to positive test if nothing specified.
- Add normalization test to travis-ci config.
- Fix container infrastructure flag from 'root' to 'sudo'.
- Update README with new testing info.
2015-10-13 13:38:56 -04:00
David I. Lehn
57b3279718 Add inputFormat option to normalize.
- Allows the input to be in N-Quads format (application/nquads) and use N-Quads
  parsing instead of a toRDF step on JSON-LD.
2015-10-13 13:31:32 -04:00
David I. Lehn
e08fe87860 Test on PHP 5.6. 2015-10-08 19:41:25 -04:00
David I. Lehn
51a0635c61 Use container testing infrastructure. 2015-10-08 19:41:25 -04:00
Dave Longley
eb4b344049 Ignore null values in language maps.
The syntax spec explicitly allows null values in language maps (section 8.5).
2015-09-14 09:40:14 -04:00
Dave Longley
2927e09639 Differentiate between mock and cache custom loaders. 2015-08-04 16:13:19 -04:00
Dave Longley
3330605897 Add examples describing how to configure document loaders. 2015-08-04 16:03:42 -04:00
Dave Longley
16155eab6a Merge pull request #4 from Tpt/composer
Add composer.json.
2015-04-06 10:22:52 -04:00
Dave Longley
989c394690 Minor style fix. 2015-03-16 13:56:04 -04:00
Hassan Almas
f23d0eb51e Fix conflicting index bug 2015-03-16 11:59:24 -04:00
Dave Longley
e599042c7d Fix typos. 2015-02-24 21:56:17 -05:00
Dave Longley
95a9945fc2 Start 0.4.7. 2015-02-07 20:08:58 -05:00
Dave Longley
7535f05755 Release 0.4.6. 2015-02-07 20:08:58 -05:00
Dave Longley
0cbff8c600 Drop null @language values when expanding.
- There's an inconsistency between the syntax spec and the API
  spec; the API spec throws an error if null is used for a
  `@language` value on Step 7.4.7 of the Expansion Algorithm,
  but the syntax spec allows it. When used, it indicates the
  value has no language -- which is the same as if `@language`
  were omitted, so it's treated the same way in this patch.
2015-02-07 20:08:58 -05:00
Tpt
aba9709907 Adds composer.json 2015-01-20 08:53:24 +01:00
David I. Lehn
0b0442696d README sync and minor updates.
- Sync README between Digital Bazaar JSON-LD libs.
- Minor updates and formatting fixes.
2014-12-05 16:45:41 -05:00
Dave Longley
2314cfe0d1 Start 0.4.6. 2014-12-05 16:41:42 -05:00
Dave Longley
906ad1d805 Release 0.4.5. 2014-12-05 16:41:29 -05:00
Dave Longley
362a8cb387 Use "array" syntax instead of "[]" shorthand. 2014-12-05 16:37:48 -05:00
David I. Lehn
e06d20062a Add travis-ci build status. 2014-12-05 16:17:16 -05:00
Dave Longley
b22dbbf285 Start 0.4.5. 2014-12-04 14:46:55 -05:00
Dave Longley
b13749ed03 Release 0.4.4. 2014-12-04 14:46:44 -05:00
Dave Longley
7be06f2ee2 Implement new experimental embed API.
- See: https://github.com/json-ld/json-ld.org/issues/377
2014-12-04 14:45:44 -05:00
Dave Longley
237a405175 Make it easier to run local (non-official) json-ld tests. 2014-12-04 14:45:17 -05:00
Dave Longley
daed05a54b Fix off-by-one in context merging. 2014-12-03 14:56:23 -05:00
Dave Longley
f252a5281c Start 0.4.4. 2014-12-03 02:07:13 -05:00
Dave Longley
b80e4b80a5 Release 0.4.3. 2014-12-03 02:06:56 -05:00
Dave Longley
1a9c6bffdd Implement URL parsing/unparsing per RFC 3986.
- Section 5.3 Component Recomposition in RFC 3986 makes a
  differentiation between undefined components and empty
  components that the built-in parse_url in python does not. This
  patch deals with that issue and ensures, for instance, that
  empty queries and fragments are detected.
2014-12-03 02:05:03 -05:00
Dave Longley
fed40914c8 Start 0.4.3. 2014-10-01 19:58:57 -04:00
Dave Longley
bf504a1506 Release 0.4.2. 2014-10-01 19:58:42 -04:00
Dave Longley
deb6afeac5 Skip array processing for keywords that aren't @graph or @list. 2014-10-01 19:58:06 -04:00
Dave Longley
ea516341be Start 0.4.2. 2014-09-15 17:05:24 -04:00
Dave Longley
d4ae67899d Release 0.4.1. 2014-09-15 17:05:08 -04:00
Dave Longley
8fb6a75e67 Ensure non-rdf:nil list node references are stored across graphs. 2014-09-15 17:04:50 -04:00
Dave Longley
8ca3291bc0 Start 0.4.1. 2014-07-29 13:41:15 -04:00
Dave Longley
d071be64f1 Release 0.4.0. 2014-07-29 13:41:05 -04:00
Dave Longley
3402d51dff Add new framing features.
- Allow filtering based on a specific @id.
- Allow ducktyping matches on @type: [{}].
- Allow ducktyping matches when a @default is specified.
- Add @requireAll flag for controlling ducktyping requirements.
2014-07-29 13:40:31 -04:00
Dave Longley
ba16da5b9f Start 0.3.8. 2014-07-29 13:17:36 -04:00
5 changed files with 6530 additions and 6013 deletions

View file

@ -1,14 +0,0 @@
language: php
php:
- 5.5
- 5.4
- 5.3
# download test suite and run tests... submodule? meta testing project with
# all of the reference implementations?
script:
- git clone https://github.com/json-ld/json-ld.org.git spec
- phpunit test.php -d spec/test-suite
notifications:
email:
on_success: change
on_failure: change

114
README.md
View file

@ -1,27 +1,29 @@
php-json-ld
===========
Introduction Introduction
------------ ------------
JSON, as specified in RFC4627, is a simple language for representing This library is an implementation of the [JSON-LD][] specification in [PHP][].
JSON, as specified in [RFC7159][], is a simple language for representing
objects on the Web. Linked Data is a way of describing content across objects on the Web. Linked Data is a way of describing content across
different documents or Web sites. Web resources are described using different documents or Web sites. Web resources are described using
IRIs, and typically are dereferencable entities that may be used to find IRIs, and typically are dereferencable entities that may be used to find
more information, creating a "Web of Knowledge". JSON-LD is intended to more information, creating a "Web of Knowledge". [JSON-LD][] is intended
be a simple publishing method for expressing not only Linked Data in to be a simple publishing method for expressing not only Linked Data in
JSON, but for adding semantics to existing JSON. JSON, but for adding semantics to existing JSON.
This library is an implementation of the [JSON-LD] specification
in [PHP].
JSON-LD is designed as a light-weight syntax that can be used to express JSON-LD is designed as a light-weight syntax that can be used to express
Linked Data. It is primarily intended to be a way to express Linked Data Linked Data. It is primarily intended to be a way to express Linked Data
in Javascript and other Web-based programming environments. It is also in JavaScript and other Web-based programming environments. It is also
useful when building interoperable Web Services and when storing Linked useful when building interoperable Web Services and when storing Linked
Data in JSON-based document storage engines. It is practical and Data in JSON-based document storage engines. It is practical and
designed to be as simple as possible, utilizing the large number of JSON designed to be as simple as possible, utilizing the large number of JSON
parsers and existing code that is in use today. It is designed to be parsers and existing code that is in use today. It is designed to be
able to express key-value pairs, RDF data, RDFa [RDFA-CORE] data, able to express key-value pairs, RDF data, [RDFa][] data,
Microformats [MICROFORMATS] data, and Microdata [MICRODATA]. That is, it [Microformats][] data, and [Microdata][]. That is, it supports every
supports every major Web-based structured data model in use today. major Web-based structured data model in use today.
The syntax does not require many applications to change their JSON, but The syntax does not require many applications to change their JSON, but
easily add meaning by adding context in a way that is either in-band or easily add meaning by adding context in a way that is either in-band or
@ -88,40 +90,92 @@ $flattened = jsonld_flatten($doc);
$framed = jsonld_frame($doc, $frame); $framed = jsonld_frame($doc, $frame);
// document transformed into a particular tree structure per the given frame // document transformed into a particular tree structure per the given frame
// normalize a document // normalize a document using the RDF Dataset Normalization Algorithm
$normalized = jsonld_normalize($doc, array('format' => 'application/nquads')); // (URDNA2015), see: http://json-ld.github.io/normalization/spec/
$normalized = jsonld_normalize(
$doc, array('algorithm' => 'URDNA2015', 'format' => 'application/nquads'));
// normalized is a string that is a canonical representation of the document // normalized is a string that is a canonical representation of the document
// that can be used for hashing // that can be used for hashing, comparison, etc.
// force HTTPS-only context loading:
// use built-in secure document loader
jsonld_set_document_loader('jsonld_default_secure_document_loader');
// set a default custom document loader
jsonld_set_document_loader('my_custom_doc_loader');
// a custom loader that demonstrates using a simple in-memory mock for
// certain contexts before falling back to the default loader
// note: if you want to set this loader as the new default, you'll need to
// store the previous default in another variable first and access that inside
// the loader
global $mocks;
$mocks = array('http://example.com/mycontext' => (object)array(
'hombre' => 'http://schema.org/name'));
function mock_load($url) {
global $jsonld_default_load_document, $mocks;
if(isset($mocks[$url])) {
// return a "RemoteDocument", it has these three properties:
return (object)array(
'contextUrl' => null,
'document' => $mocks[$url],
'documentUrl' => $url);
}
// use default loader
return call_user_func($jsonld_default_load_document, $url);
}
// use the mock loader for just this call, witout modifying the default one
$compacted = jsonld_compact($foo, 'http://example.com/mycontext', array(
'documentLoader' => 'mock_load'));
// a custom loader that uses a simplistic in-memory cache (no invalidation)
global $cache;
$cache = array();
function cache_load($url) {
global $jsonld_default_load_document, $cache;
if(isset($cache[$url])) {
return $cache[$url];
}
// use default loader
$doc = call_user_func($jsonld_default_load_document, $url);
$cache[$url] = $doc;
return $doc;
}
// use the cache loader for just this call, witout modifying the default one
$compacted = jsonld_compact($foo, 'http://schema.org', array(
'documentLoader' => 'cache_load'));
``` ```
Commercial Support
------------------
Commercial support for this library is available upon request from
Digital Bazaar: support@digitalbazaar.com
Source Source
------ ------
The source code for the PHP implementation of the JSON-LD API The source code for the PHP implementation of the JSON-LD API is available at:
is available at:
http://github.com/digitalbazaar/php-json-ld https://git.friendi.ca/friendica/php-json-ld
Tests
-----
This library includes a sample testing utility which may be used to verify This library includes a sample testing utility which may be used to verify
that changes to the processor maintain the correct output. that changes to the processor maintain the correct output.
To run the sample tests you will need to get the test suite files by cloning To run the sample tests you will need to get the test suite files by cloning
the [json-ld.org repository][json-ld.org] hosted on GitHub. the `json-ld.org` and `normalization` repositories hosted on GitHub:
https://github.com/json-ld/json-ld.org - https://github.com/json-ld/json-ld.org
- https://github.com/json-ld/normalization
Then run the PHPUnit test.php application and point it at the directory Then run the PHPUnit test.php application and point it at the directories
containing the tests. containing the tests:
phpunit test.php -d {PATH_TO_JSON_LD_ORG/test-suite} phpunit --group json-ld.org test.php -d {PATH_TO_JSON_LD_ORG/test-suite}
phpunit --group normalization test.php -d {PATH_TO_NORMALIZATION/tests}
[PHP]: http://php.net
[JSON-LD]: http://json-ld.org/ [JSON-LD]: http://json-ld.org/
[json-ld.org]: https://github.com/json-ld/json-ld.org [Microdata]: http://www.w3.org/TR/microdata/
[Microformats]: http://microformats.org/
[PHP]: http://php.net
[RDFa]: http://www.w3.org/TR/rdfa-core/
[RFC7159]: http://tools.ietf.org/html/rfc7159

33
composer.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "friendica/json-ld",
"type": "library",
"description": "A JSON-LD Processor and API implementation in PHP.",
"keywords": [
"JSON",
"Linked Data",
"JSON-LD",
"RDF",
"Semantic Web",
"jsonld"
],
"homepage": "https://git.friendi.ca/friendica/php-json-ld",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Digital Bazaar, Inc.",
"email": "support@digitalbazaar.com",
"homepage": "http://digitalbazaar.com/"
},
{
"name": "Friendica Team",
"homepage": "https://friendi.ca/"
}
],
"require": {
"php": ">=5.4.0",
"ext-json": "*"
},
"autoload": {
"files": ["jsonld.php"]
}
}

11079
jsonld.php
View file

@ -1,9 +1,11 @@
<?php <?php
/** /**
* PHP implementation of the JSON-LD API. * PHP implementation of the JSON-LD API.
* Version: 0.3.7 * Version: 1.1.0
* *
* @author Dave Longley * @author Dave Longley
* @author Michael Vogel
* *
* BSD 3-Clause License * BSD 3-Clause License
* Copyright (c) 2011-2014 Digital Bazaar, Inc. * Copyright (c) 2011-2014 Digital Bazaar, Inc.
@ -48,9 +50,10 @@
* *
* @return mixed the compacted JSON-LD output. * @return mixed the compacted JSON-LD output.
*/ */
function jsonld_compact($input, $ctx, $options=array()) { function jsonld_compact($input, $ctx, $options = [])
$p = new JsonLdProcessor(); {
return $p->compact($input, $ctx, $options); $p = new JsonLdProcessor();
return $p->compact($input, $ctx, $options);
} }
/** /**
@ -63,9 +66,10 @@ function jsonld_compact($input, $ctx, $options=array()) {
* *
* @return array the expanded JSON-LD output. * @return array the expanded JSON-LD output.
*/ */
function jsonld_expand($input, $options=array()) { function jsonld_expand($input, $options = [])
$p = new JsonLdProcessor(); {
return $p->expand($input, $options); $p = new JsonLdProcessor();
return $p->expand($input, $options);
} }
/** /**
@ -80,9 +84,10 @@ function jsonld_expand($input, $options=array()) {
* *
* @return mixed the flattened JSON-LD output. * @return mixed the flattened JSON-LD output.
*/ */
function jsonld_flatten($input, $ctx, $options=array()) { function jsonld_flatten($input, $ctx, $options = [])
$p = new JsonLdProcessor(); {
return $p->flatten($input, $ctx, $options); $p = new JsonLdProcessor();
return $p->flatten($input, $ctx, $options);
} }
/** /**
@ -94,32 +99,64 @@ function jsonld_flatten($input, $ctx, $options=array()) {
* [base] the base IRI to use. * [base] the base IRI to use.
* [embed] default @embed flag (default: true). * [embed] default @embed flag (default: true).
* [explicit] default @explicit flag (default: false). * [explicit] default @explicit flag (default: false).
* [requireAll] default @requireAll flag (default: true).
* [omitDefault] default @omitDefault flag (default: false). * [omitDefault] default @omitDefault flag (default: false).
* [documentLoader(url)] the document loader. * [documentLoader(url)] the document loader.
* *
* @return stdClass the framed JSON-LD output. * @return stdClass the framed JSON-LD output.
*/ */
function jsonld_frame($input, $frame, $options=array()) { function jsonld_frame($input, $frame, $options = [])
$p = new JsonLdProcessor(); {
return $p->frame($input, $frame, $options); $p = new JsonLdProcessor();
return $p->frame($input, $frame, $options);
} }
/** /**
* Performs RDF dataset normalization on the given JSON-LD input. The output * **Experimental**
* is an RDF dataset unless the 'format' option is used. *
* Links a JSON-LD document's nodes in memory.
*
* @param mixed $input the JSON-LD document to link.
* @param mixed $ctx the JSON-LD context to apply or null.
* @param assoc [$options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [documentLoader(url)] the document loader.
*
* @return the linked JSON-LD output.
*/
function jsonld_link($input, $ctx, $options)
{
// API matches running frame with a wildcard frame and embed: '@link'
// get arguments
$frame = new stdClass();
if ($ctx) {
$frame->{'@context'} = $ctx;
}
$frame->{'@embed'} = '@link';
return jsonld_frame($input, $frame, $options);
}
/**
* Performs RDF dataset normalization on the given input. The input is
* JSON-LD unless the 'inputFormat' option is used. The output is an RDF
* dataset unless the 'format' option is used.
* *
* @param mixed $input the JSON-LD object to normalize. * @param mixed $input the JSON-LD object to normalize.
* @param assoc [$options] the options to use: * @param assoc [$options] the options to use:
* [base] the base IRI to use. * [base] the base IRI to use.
* [intputFormat] the format if input is not JSON-LD:
* 'application/nquads' for N-Quads.
* [format] the format if output is a string: * [format] the format if output is a string:
* 'application/nquads' for N-Quads. * 'application/nquads' for N-Quads.
* [documentLoader(url)] the document loader. * [documentLoader(url)] the document loader.
* *
* @return mixed the normalized output. * @return mixed the normalized output.
*/ */
function jsonld_normalize($input, $options=array()) { function jsonld_normalize($input, $options = [])
$p = new JsonLdProcessor(); {
return $p->normalize($input, $options); $p = new JsonLdProcessor();
return $p->normalize($input, $options);
} }
/** /**
@ -137,9 +174,10 @@ function jsonld_normalize($input, $options=array()) {
* *
* @return array the JSON-LD output. * @return array the JSON-LD output.
*/ */
function jsonld_from_rdf($input, $options=array()) { function jsonld_from_rdf($input, $options = [])
$p = new JsonLdProcessor(); {
return $p->fromRDF($input, $options); $p = new JsonLdProcessor();
return $p->fromRDF($input, $options);
} }
/** /**
@ -156,9 +194,10 @@ function jsonld_from_rdf($input, $options=array()) {
* *
* @return mixed the resulting RDF dataset (or a serialization of it). * @return mixed the resulting RDF dataset (or a serialization of it).
*/ */
function jsonld_to_rdf($input, $options=array()) { function jsonld_to_rdf($input, $options = [])
$p = new JsonLdProcessor(); {
return $p->toRDF($input, $options); $p = new JsonLdProcessor();
return $p->toRDF($input, $options);
} }
/** /**
@ -172,13 +211,14 @@ function jsonld_to_rdf($input, $options=array()) {
* *
* @return the encoded JSON data. * @return the encoded JSON data.
*/ */
function jsonld_encode($input, $options=0, $depth=512) { function jsonld_encode($input, $options = 0, $depth = 512)
// newer PHP has a flag to avoid escaped '/' {
if(defined('JSON_UNESCAPED_SLASHES')) { // newer PHP has a flag to avoid escaped '/'
return json_encode($input, JSON_UNESCAPED_SLASHES | $options, $depth); if (defined('JSON_UNESCAPED_SLASHES')) {
} return json_encode($input, JSON_UNESCAPED_SLASHES | $options, $depth);
// use a simple string replacement of '\/' to '/'. }
return str_replace('\\/', '/', json_encode($input, $options, $depth)); // use a simple string replacement of '\/' to '/'.
return str_replace('\\/', '/', json_encode($input, $options, $depth));
} }
/** /**
@ -188,8 +228,9 @@ function jsonld_encode($input, $options=0, $depth=512) {
* *
* @return mixed the resolved JSON-LD object, null on error. * @return mixed the resolved JSON-LD object, null on error.
*/ */
function jsonld_decode($input) { function jsonld_decode($input)
return json_decode($input); {
return json_decode($input);
} }
/** /**
@ -212,35 +253,37 @@ function jsonld_decode($input) {
* *
* @return assoc the parsed result. * @return assoc the parsed result.
*/ */
function jsonld_parse_link_header($header) { function jsonld_parse_link_header($header)
$rval = array(); {
// split on unbracketed/unquoted commas $rval = [];
if(!preg_match_all( // split on unbracketed/unquoted commas
'/(?:<[^>]*?>|"[^"]*?"|[^,])+/', $header, $entries, PREG_SET_ORDER)) { if (!preg_match_all('/(?:<[^>]*?>|"[^"]*?"|[^,])+/', $header, $entries, PREG_SET_ORDER)) {
return $rval; return $rval;
} }
$r_link_header = '/\s*<([^>]*?)>\s*(?:;\s*(.*))?/';
foreach($entries as $entry) { $r_link_header = '/\s*<([^>]*?)>\s*(?:;\s*(.*))?/';
if(!preg_match($r_link_header, $entry[0], $match)) { foreach ($entries as $entry) {
continue; if (!preg_match($r_link_header, $entry[0], $match)) {
} continue;
$result = (object)array('target' => $match[1]); }
$params = $match[2]; $result = (object) ['target' => $match[1]];
$r_params = '/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/'; $params = $match[2];
preg_match_all($r_params, $params, $matches, PREG_SET_ORDER); $r_params = '/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/';
foreach($matches as $match) { preg_match_all($r_params, $params, $matches, PREG_SET_ORDER);
$result->{$match[1]} = $match[2] ?: $match[3]; foreach ($matches as $match) {
} $result->{$match[1]} = $match[2] ?: $match[3];
$rel = property_exists($result, 'rel') ? $result->rel : ''; }
if(!isset($rval[$rel])) {
$rval[$rel] = $result; $rel = property_exists($result, 'rel') ? $result->rel : '';
} else if(is_array($rval[$rel])) { if (!isset($rval[$rel])) {
$rval[$rel][] = $result; $rval[$rel] = $result;
} else { } else if (is_array($rval[$rel])) {
$rval[$rel] = array($rval[$rel], $result); $rval[$rel][] = $result;
} } else {
} $rval[$rel] = [$rval[$rel], $result];
return $rval; }
}
return $rval;
} }
/** /**
@ -248,9 +291,10 @@ function jsonld_parse_link_header($header) {
* *
* @param mixed input the JSON-LD input. * @param mixed input the JSON-LD input.
*/ */
function jsonld_relabel_blank_nodes($input) { function jsonld_relabel_blank_nodes($input)
$p = new JsonLdProcessor(); {
return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input); $p = new JsonLdProcessor();
return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input);
} }
/** JSON-LD shared in-memory cache. */ /** JSON-LD shared in-memory cache. */
@ -269,9 +313,10 @@ $jsonld_default_load_document = 'jsonld_default_document_loader';
* *
* @param callable load_document(url) the document loader. * @param callable load_document(url) the document loader.
*/ */
function jsonld_set_document_loader($load_document) { function jsonld_set_document_loader($load_document)
global $jsonld_default_load_document; {
$jsonld_default_load_document = $load_document; global $jsonld_default_load_document;
$jsonld_default_load_document = $load_document;
} }
/** /**
@ -281,19 +326,21 @@ function jsonld_set_document_loader($load_document) {
* *
* @return the JSON-LD. * @return the JSON-LD.
*/ */
function jsonld_get_url($url) { function jsonld_get_url($url)
global $jsonld_default_load_document; {
if($jsonld_default_load_document !== null) { global $jsonld_default_load_document;
$document_loader = $jsonld_default_load_document; if ($jsonld_default_load_document !== null) {
} else { $document_loader = $jsonld_default_load_document;
$document_loader = 'jsonld_default_document_loader'; } else {
} $document_loader = 'jsonld_default_document_loader';
}
$remote_doc = call_user_func($document_loader, $url); $remote_doc = call_user_func($document_loader, $url);
if($remote_doc) { if ($remote_doc) {
return $remote_doc->document; return $remote_doc->document;
} }
return null;
return null;
} }
/** /**
@ -303,76 +350,80 @@ function jsonld_get_url($url) {
* *
* @return stdClass the RemoteDocument object. * @return stdClass the RemoteDocument object.
*/ */
function jsonld_default_document_loader($url) { function jsonld_default_document_loader($url)
$doc = (object)array( {
'contextUrl' => null, 'document' => null, 'documentUrl' => $url); $doc = (object) [
$redirects = array(); 'contextUrl' => null, 'document' => null, 'documentUrl' => $url];
$redirects = [];
$opts = array( $opts = [
'http' => array( 'http' => [
'method' => 'GET', 'method' => 'GET',
'header' => 'header' =>
"Accept: application/ld+json\r\n"), "Accept: application/ld+json\r\n"],
/* Note: Use jsonld_default_secure_document_loader for security. */ /* Note: Use jsonld_default_secure_document_loader for security. */
'ssl' => array( 'ssl' => [
'verify_peer' => false, 'verify_peer' => false,
'allow_self_signed' => true) 'allow_self_signed' => true]
); ];
$context = stream_context_create($opts); $context = stream_context_create($opts);
$content_type = null; $content_type = null;
stream_context_set_params($context, array('notification' => stream_context_set_params($context, ['notification' =>
function($notification_code, $severity, $message) use ( function($notification_code, $severity, $message) use (
&$redirects, &$content_type) { &$redirects, &$content_type) {
switch($notification_code) { switch ($notification_code) {
case STREAM_NOTIFY_REDIRECTED: case STREAM_NOTIFY_REDIRECTED:
$redirects[] = $message; $redirects[] = $message;
break; break;
case STREAM_NOTIFY_MIME_TYPE_IS: case STREAM_NOTIFY_MIME_TYPE_IS:
$content_type = $message; $content_type = $message;
break; break;
}; };
})); }]);
$result = @file_get_contents($url, false, $context); $result = @file_get_contents($url, false, $context);
if($result === false) { if ($result === false) {
throw new JsonLdException( throw new JsonLdException(
'Could not retrieve a JSON-LD document from the URL: ' . $url, 'Could not retrieve a JSON-LD document from the URL: ' . $url, 'jsonld.LoadDocumentError', 'loading document failed');
'jsonld.LoadDocumentError', 'loading document failed'); }
}
$link_header = array();
foreach($http_response_header as $header) {
if(strpos($header, 'link') === 0) {
$value = explode(': ', $header);
if(count($value) > 1) {
$link_header[] = $value[1];
}
}
}
$link_header = jsonld_parse_link_header(join(',', $link_header));
if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
$link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
} else {
$link_header = null;
}
if($link_header && $content_type !== 'application/ld+json') {
// only 1 related link header permitted
if(is_array($link_header)) {
throw new JsonLdException(
'URL could not be dereferenced, it has more than one ' .
'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
'multiple context link headers', array('url' => $url));
}
$doc->{'contextUrl'} = $link_header->target;
}
// update document url based on redirects $link_header = [];
$redirs = count($redirects); foreach ($http_response_header as $header) {
if($redirs > 0) { if (strpos($header, 'link') === 0) {
$url = $redirects[$redirs - 1]; $value = explode(': ', $header);
} if (count($value) > 1) {
$doc->document = $result; $link_header[] = $value[1];
$doc->documentUrl = $url; }
return $doc; }
}
$link_header = jsonld_parse_link_header(join(',', $link_header));
if (isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
$link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
} else {
$link_header = null;
}
if ($link_header && $content_type !== 'application/ld+json') {
// only 1 related link header permitted
if (is_array($link_header)) {
throw new JsonLdException(
'URL could not be dereferenced, it has more than one ' .
'associated HTTP Link Header.', 'jsonld.LoadDocumentError', 'multiple context link headers', ['url' => $url]);
}
$doc->{'contextUrl'} = $link_header->target;
}
// update document url based on redirects
$redirs = count($redirects);
if ($redirs > 0) {
$url = $redirects[$redirs - 1];
}
$doc->document = $result;
$doc->documentUrl = $url;
return $doc;
} }
/** /**
@ -382,85 +433,92 @@ function jsonld_default_document_loader($url) {
* *
* @return stdClass the RemoteDocument object. * @return stdClass the RemoteDocument object.
*/ */
function jsonld_default_secure_document_loader($url) { function jsonld_default_secure_document_loader($url)
if(strpos($url, 'https') !== 0) { {
throw new JsonLdException( if (strpos($url, 'https') !== 0) {
"Could not GET url: '$url'; 'https' is required.", throw new JsonLdException(
'jsonld.LoadDocumentError', 'loading document failed'); "Could not GET url: '$url'; 'https' is required.",
} 'jsonld.LoadDocumentError',
'loading document failed'
);
}
$doc = (object)array( $doc = (object) [
'contextUrl' => null, 'document' => null, 'documentUrl' => $url); 'contextUrl' => null, 'document' => null, 'documentUrl' => $url];
$redirects = array(); $redirects = [];
// default JSON-LD https GET implementation // default JSON-LD https GET implementation
$opts = array( $opts = [
'http' => array( 'http' => [
'method' => 'GET', 'method' => 'GET',
'header' => 'header' =>
"Accept: application/ld+json\r\n"), "Accept: application/ld+json\r\n"],
'ssl' => array( 'ssl' => [
'verify_peer' => true, 'verify_peer' => true,
'allow_self_signed' => false, 'allow_self_signed' => false,
'cafile' => '/etc/ssl/certs/ca-certificates.crt')); 'cafile' => '/etc/ssl/certs/ca-certificates.crt']];
$context = stream_context_create($opts); $context = stream_context_create($opts);
$content_type = null; $content_type = null;
stream_context_set_params($context, array('notification' => stream_context_set_params($context, ['notification' =>
function($notification_code, $severity, $message) use ( function($notification_code, $severity, $message) use (
&$redirects, &$content_type) { &$redirects, &$content_type) {
switch($notification_code) { switch ($notification_code) {
case STREAM_NOTIFY_REDIRECTED: case STREAM_NOTIFY_REDIRECTED:
$redirects[] = $message; $redirects[] = $message;
break; break;
case STREAM_NOTIFY_MIME_TYPE_IS: case STREAM_NOTIFY_MIME_TYPE_IS:
$content_type = $message; $content_type = $message;
break; break;
}; };
})); }]);
$result = @file_get_contents($url, false, $context); $result = @file_get_contents($url, false, $context);
if($result === false) { if ($result === false) {
throw new JsonLdException( throw new JsonLdException(
'Could not retrieve a JSON-LD document from the URL: ' + $url, 'Could not retrieve a JSON-LD document from the URL: ' + $url, 'jsonld.LoadDocumentError', 'loading document failed');
'jsonld.LoadDocumentError', 'loading document failed'); }
}
$link_header = array();
foreach($http_response_header as $header) {
if(strpos($header, 'link') === 0) {
$value = explode(': ', $header);
if(count($value) > 1) {
$link_header[] = $value[1];
}
}
}
$link_header = jsonld_parse_link_header(join(',', $link_header));
if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
$link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
} else {
$link_header = null;
}
if($link_header && $content_type !== 'application/ld+json') {
// only 1 related link header permitted
if(is_array($link_header)) {
throw new JsonLdException(
'URL could not be dereferenced, it has more than one ' .
'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
'multiple context link headers', array('url' => $url));
}
$doc->{'contextUrl'} = $link_header->target;
}
// update document url based on redirects $link_header = [];
foreach($redirects as $redirect) { foreach ($http_response_header as $header) {
if(strpos($redirect, 'https') !== 0) { if (strpos($header, 'link') === 0) {
throw new JsonLdException( $value = explode(': ', $header);
"Could not GET redirected url: '$redirect'; 'https' is required.", if (count($value) > 1) {
'jsonld.LoadDocumentError', 'loading document failed'); $link_header[] = $value[1];
} }
$url = $redirect; }
} }
$doc->document = $result;
$doc->documentUrl = $url; $link_header = jsonld_parse_link_header(join(',', $link_header));
return $doc; if (isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
$link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
} else {
$link_header = null;
}
if ($link_header && $content_type !== 'application/ld+json') {
// only 1 related link header permitted
if (is_array($link_header)) {
throw new JsonLdException(
'URL could not be dereferenced, it has more than one ' .
'associated HTTP Link Header.', 'jsonld.LoadDocumentError', 'multiple context link headers', ['url' => $url]);
}
$doc->{'contextUrl'} = $link_header->target;
}
// update document url based on redirects
foreach ($redirects as $redirect) {
if (strpos($redirect, 'https') !== 0) {
throw new JsonLdException(
"Could not GET redirected url: '$redirect'; 'https' is required.",
'jsonld.LoadDocumentError',
'loading document failed'
);
}
$url = $redirect;
}
$doc->document = $result;
$doc->documentUrl = $url;
return $doc;
} }
/** Registered global RDF dataset parsers hashed by content-type. */ /** Registered global RDF dataset parsers hashed by content-type. */
@ -476,9 +534,10 @@ $jsonld_rdf_parsers = new stdClass();
* @param callable $parser(input) the parser function (takes a string as * @param callable $parser(input) the parser function (takes a string as
* a parameter and returns an RDF dataset). * a parameter and returns an RDF dataset).
*/ */
function jsonld_register_rdf_parser($content_type, $parser) { function jsonld_register_rdf_parser($content_type, $parser)
global $jsonld_rdf_parsers; {
$jsonld_rdf_parsers->{$content_type} = $parser; global $jsonld_rdf_parsers;
$jsonld_rdf_parsers->{$content_type} = $parser;
} }
/** /**
@ -486,11 +545,12 @@ function jsonld_register_rdf_parser($content_type, $parser) {
* *
* @param string $content_type the content-type for the parser. * @param string $content_type the content-type for the parser.
*/ */
function jsonld_unregister_rdf_parser($content_type) { function jsonld_unregister_rdf_parser($content_type)
global $jsonld_rdf_parsers; {
if(property_exists($jsonld_rdf_parsers, $content_type)) { global $jsonld_rdf_parsers;
unset($jsonld_rdf_parsers->{$content_type}); if (property_exists($jsonld_rdf_parsers, $content_type)) {
} unset($jsonld_rdf_parsers->{$content_type});
}
} }
/** /**
@ -500,66 +560,53 @@ function jsonld_unregister_rdf_parser($content_type) {
* *
* @return assoc the parsed URL. * @return assoc the parsed URL.
*/ */
function jsonld_parse_url($url) { function jsonld_parse_url($url)
if($url === null) { {
$url = ''; if ($url === null) {
} $url = '';
}
$rval = parse_url($url); $keys = [
'href', 'protocol', 'scheme', '?authority', 'authority',
'?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path',
'?query', 'query', '?fragment', 'fragment'];
$regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/";
preg_match($regex, $url, $match);
// malformed url $rval = [];
if($rval === false) { $flags = [];
$rval = array(); $len = count($keys);
} for ($i = 0; $i < $len; ++$i) {
$key = $keys[$i];
if (strpos($key, '?') === 0) {
$flags[substr($key, 1)] = !empty($match[$i]);
} else if (!isset($match[$i])) {
$rval[$key] = null;
} else {
$rval[$key] = $match[$i];
}
}
$rval['href'] = $url; if (!$flags['authority']) {
if(!isset($rval['scheme'])) { $rval['authority'] = null;
$rval['scheme'] = ''; }
$rval['protocol'] = ''; if (!$flags['auth']) {
} else { $rval['auth'] = $rval['user'] = $rval['pass'] = null;
$rval['protocol'] = $rval['scheme'] . ':'; }
} if (!$flags['port']) {
if(!isset($rval['host'])) { $rval['port'] = null;
$rval['host'] = ''; }
} if (!$flags['query']) {
if(!isset($rval['path'])) { $rval['query'] = null;
$rval['path'] = ''; }
} if (!$flags['fragment']) {
if(isset($rval['user']) || isset($rval['pass'])) { $rval['fragment'] = null;
$rval['auth'] = ''; }
if(isset($rval['user'])) {
$rval['auth'] = $rval['user'];
}
if(isset($rval['pass'])) {
$rval['auth'] .= ":{$rval['pass']}";
}
}
// parse authority for unparsed relative network-path reference
if(strpos($rval['href'], ':') === false &&
strpos($rval['href'], '//') === 0 && $rval['host'] === '') {
// must parse authority from pathname
$rval['path'] = substr($rval['path'], 2);
$idx = strpos($rval['path'], '/');
if($idx === false) {
$rval['authority'] = $rval['path'];
$rval['path'] = '';
} else {
$rval['authority'] = substr($rval['path'], 0, $idx);
$rval['path'] = substr($rval['path'], $idx);
}
} else {
$rval['authority'] = $rval['host'];
if(isset($rval['port'])) {
$rval['authority'] .= ":{$rval['port']}";
}
if(isset($rval['auth'])) {
$rval['authority'] = "{$rval['auth']}@{$rval['authority']}";
}
}
$rval['normalizedPath'] = jsonld_remove_dot_segments(
$rval['path'], $rval['authority'] !== '');
return $rval; $rval['normalizedPath'] = jsonld_remove_dot_segments(
$rval['path'], !!$rval['authority']);
return $rval;
} }
/** /**
@ -568,36 +615,37 @@ function jsonld_parse_url($url) {
* @param string $path the path to remove dot segments from. * @param string $path the path to remove dot segments from.
* @param bool $has_authority true if the URL has an authority, false if not. * @param bool $has_authority true if the URL has an authority, false if not.
*/ */
function jsonld_remove_dot_segments($path, $has_authority) { function jsonld_remove_dot_segments($path, $has_authority)
$rval = ''; {
$rval = '';
if(strpos($path, '/') === 0) { if (strpos($path, '/') === 0) {
$rval = '/'; $rval = '/';
} }
// RFC 3986 5.2.4 (reworked) // RFC 3986 5.2.4 (reworked)
$input = explode('/', $path); $input = explode('/', $path);
$output = array(); $output = [];
while(count($input) > 0) { while (count($input) > 0) {
if($input[0] === '.' || ($input[0] === '' && count($input) > 1)) { if ($input[0] === '.' || ($input[0] === '' && count($input) > 1)) {
array_shift($input); array_shift($input);
continue; continue;
} }
if($input[0] === '..') { if ($input[0] === '..') {
array_shift($input); array_shift($input);
if($has_authority || if ($has_authority ||
(count($output) > 0 && $output[count($output) - 1] !== '..')) { (count($output) > 0 && $output[count($output) - 1] !== '..')) {
array_pop($output); array_pop($output);
} else { } else {
// leading relative URL '..' // leading relative URL '..'
$output[] = '..'; $output[] = '..';
} }
continue; continue;
} }
$output[] = array_shift($input); $output[] = array_shift($input);
} }
return $rval . implode('/', $output); return $rval . implode('/', $output);
} }
/** /**
@ -608,71 +656,91 @@ function jsonld_remove_dot_segments($path, $has_authority) {
* *
* @return string the absolute IRI. * @return string the absolute IRI.
*/ */
function jsonld_prepend_base($base, $iri) { function jsonld_prepend_base($base, $iri)
// skip IRI processing {
if($base === null) { // skip IRI processing
return $iri; if ($base === null) {
} return $iri;
}
// already an absolute IRI // already an absolute IRI
if(strpos($iri, ':') !== false) { if (strpos($iri, ':') !== false) {
return $iri; return $iri;
} }
// parse base if it is a string // parse base if it is a string
if(is_string($base)) { if (is_string($base)) {
$base = jsonld_parse_url($base); $base = jsonld_parse_url($base);
} }
// parse given IRI // parse given IRI
$rel = jsonld_parse_url($iri); $rel = jsonld_parse_url($iri);
// start hierarchical part // per RFC3986 5.2.2
$hierPart = $base['protocol']; $transform = ['protocol' => $base['protocol']];
if($rel['authority']) {
$hierPart .= "//{$rel['authority']}";
} else if($base['href'] !== '') {
$hierPart .= "//{$base['authority']}";
}
// per RFC3986 normalize if ($rel['authority'] !== null) {
$transform['authority'] = $rel['authority'];
$transform['path'] = $rel['path'];
$transform['query'] = $rel['query'];
} else {
$transform['authority'] = $base['authority'];
// IRI represents an absolute path if ($rel['path'] === '') {
if(strpos($rel['path'], '/') === 0) { $transform['path'] = $base['path'];
$path = $rel['path']; if ($rel['query'] !== null) {
} else { $transform['query'] = $rel['query'];
$path = $base['path']; } else {
$transform['query'] = $base['query'];
}
} else {
if (strpos($rel['path'], '/') === 0) {
// IRI represents an absolute path
$transform['path'] = $rel['path'];
} else {
// merge paths
$path = $base['path'];
// append relative path to the end of the last directory from base // append relative path to the end of the last directory from base
if($rel['path'] !== '') { if ($rel['path'] !== '') {
$idx = strrpos($path, '/'); $idx = strrpos($path, '/');
$idx = ($idx === false) ? 0 : $idx + 1; $idx = ($idx === false) ? 0 : $idx + 1;
$path = substr($path, 0, $idx); $path = substr($path, 0, $idx);
if(strlen($path) > 0 && substr($path, -1) !== '/') { if (strlen($path) > 0 && substr($path, -1) !== '/') {
$path .= '/'; $path .= '/';
} }
$path .= $rel['path']; $path .= $rel['path'];
} }
}
// remove slashes and dots in path $transform['path'] = $path;
$path = jsonld_remove_dot_segments($path, $hierPart !== ''); }
$transform['query'] = $rel['query'];
}
}
// add query and hash // remove slashes and dots in path
if(isset($rel['query'])) { $transform['path'] = jsonld_remove_dot_segments(
$path .= "?{$rel['query']}"; $transform['path'], !!$transform['authority']);
}
if(isset($rel['fragment'])) {
$path .= "#{$rel['fragment']}";
}
$rval = $hierPart . $path; // construct URL
$rval = $transform['protocol'];
if ($transform['authority'] !== null) {
$rval .= '//' . $transform['authority'];
}
$rval .= $transform['path'];
if ($transform['query'] !== null) {
$rval .= '?' . $transform['query'];
}
if ($rel['fragment'] !== null) {
$rval .= '#' . $rel['fragment'];
}
if($rval === '') { // handle empty base
$rval = './'; if ($rval === '') {
} $rval = './';
}
return $rval; return $rval;
} }
/** /**
@ -684,5113 +752,5286 @@ function jsonld_prepend_base($base, $iri) {
* @return string the relative IRI if relative to base, otherwise the absolute * @return string the relative IRI if relative to base, otherwise the absolute
* IRI. * IRI.
*/ */
function jsonld_remove_base($base, $iri) { function jsonld_remove_base($base, $iri)
// skip IRI processing {
if($base === null) { // skip IRI processing
return $iri; if ($base === null) {
} return $iri;
}
if(is_string($base)) { if (is_string($base)) {
$base = jsonld_parse_url($base); $base = jsonld_parse_url($base);
} }
// establish base root // establish base root
$root = ''; $root = '';
if($base['href'] !== '') { if ($base['href'] !== '') {
$root .= "{$base['protocol']}//{$base['authority']}"; $root .= "{$base['protocol']}//{$base['authority']}";
} else if(strpos($iri, '//') === false) { } else if (strpos($iri, '//') === false) {
// support network-path reference with empty base // support network-path reference with empty base
$root .= '//'; $root .= '//';
} }
// IRI not relative to base // IRI not relative to base
if($root === '' || strpos($iri, $root) !== 0) { if ($root === '' || strpos($iri, $root) !== 0) {
return $iri; return $iri;
} }
// remove root from IRI // remove root from IRI
$rel = jsonld_parse_url(substr($iri, strlen($root))); $rel = jsonld_parse_url(substr($iri, strlen($root)));
// remove path segments that match (do not remove last segment unless there // remove path segments that match (do not remove last segment unless there
// is a hash or query) // is a hash or query)
$base_segments = explode('/', $base['normalizedPath']); $base_segments = explode('/', $base['normalizedPath']);
$iri_segments = explode('/', $rel['normalizedPath']); $iri_segments = explode('/', $rel['normalizedPath']);
$last = (isset($rel['query']) || isset($rel['fragment'])) ? 0 : 1; $last = ($rel['query'] || $rel['fragment']) ? 0 : 1;
while(count($base_segments) > 0 && count($iri_segments) > $last) { while (count($base_segments) > 0 && count($iri_segments) > $last) {
if($base_segments[0] !== $iri_segments[0]) { if ($base_segments[0] !== $iri_segments[0]) {
break; break;
} }
array_shift($base_segments); array_shift($base_segments);
array_shift($iri_segments); array_shift($iri_segments);
} }
// use '../' for each non-matching base segment // use '../' for each non-matching base segment
$rval = ''; $rval = '';
if(count($base_segments) > 0) { if (count($base_segments) > 0) {
// don't count the last segment (if it ends with '/' last path doesn't // don't count the last segment (if it ends with '/' last path doesn't
// count and if it doesn't end with '/' it isn't a path) // count and if it doesn't end with '/' it isn't a path)
array_pop($base_segments); array_pop($base_segments);
foreach($base_segments as $segment) { foreach ($base_segments as $segment) {
$rval .= '../'; $rval .= '../';
} }
} }
// prepend remaining segments // prepend remaining segments
$rval .= implode('/', $iri_segments); $rval .= implode('/', $iri_segments);
// add query and hash // add query and hash
if(isset($rel['query'])) { if ($rel['query'] !== null) {
$rval .= "?{$rel['query']}"; $rval .= "?{$rel['query']}";
} }
if(isset($rel['fragment'])) { if ($rel['fragment'] !== null) {
$rval .= "#{$rel['fragment']}"; $rval .= "#{$rel['fragment']}";
} }
if($rval === '') { if ($rval === '') {
$rval = './'; $rval = './';
} }
return $rval; return $rval;
} }
/** /**
* A JSON-LD processor. * A JSON-LD processor.
*/ */
class JsonLdProcessor { class JsonLdProcessor
/** XSD constants */ {
const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double'; /** XSD constants */
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer'; const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string'; const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
/** RDF constants */ const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
const RDF_LIST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#List';
const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first'; /** RDF constants */
const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'; const RDF_LIST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#List';
const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'; const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest';
const RDF_LANGSTRING = const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil';
'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString'; const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
const RDF_LANGSTRING = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString';
/** Restraints */
const MAX_CONTEXT_URLS = 10; /** Restraints */
const MAX_CONTEXT_URLS = 10;
/** Processor-specific RDF dataset parsers. */
protected $rdfParsers = null; /** Processor-specific RDF dataset parsers. */
protected $rdfParsers = null;
/**
* Constructs a JSON-LD processor. /**
*/ * Constructs a JSON-LD processor.
public function __construct() {} */
public function __construct()
/** {
* Performs JSON-LD compaction.
* }
* @param mixed $input the JSON-LD object to compact.
* @param mixed $ctx the context to compact with. /**
* @param assoc $options the compaction options. * Performs JSON-LD compaction.
* [base] the base IRI to use. *
* [compactArrays] true to compact arrays to single values when * @param mixed $input the JSON-LD object to compact.
* appropriate, false not to (default: true). * @param mixed $ctx the context to compact with.
* [graph] true to always output a top-level graph (default: false). * @param assoc $options the compaction options.
* [skipExpansion] true to assume the input is expanded and skip * [base] the base IRI to use.
* expansion, false not to, defaults to false. * [compactArrays] true to compact arrays to single values when
* [activeCtx] true to also return the active context used. * appropriate, false not to (default: true).
* [documentLoader(url)] the document loader. * [graph] true to always output a top-level graph (default: false).
* * [skipExpansion] true to assume the input is expanded and skip
* @return mixed the compacted JSON-LD output. * expansion, false not to, defaults to false.
*/ * [activeCtx] true to also return the active context used.
public function compact($input, $ctx, $options) { * [documentLoader(url)] the document loader.
global $jsonld_default_load_document; *
* @return mixed the compacted JSON-LD output.
if($ctx === null) { */
throw new JsonLdException( public function compact($input, $ctx, $options)
'The compaction context must not be null.', {
'jsonld.CompactError', 'invalid local context'); global $jsonld_default_load_document;
}
if ($ctx === null) {
// nothing to compact throw new JsonLdException(
if($input === null) { 'The compaction context must not be null.', 'jsonld.CompactError', 'invalid local context');
return null; }
}
// nothing to compact
self::setdefaults($options, array( if ($input === null) {
'base' => is_string($input) ? $input : '', return null;
'compactArrays' => true, }
'graph' => false,
'skipExpansion' => false, self::setdefaults($options, [
'activeCtx' => false, 'base' => is_string($input) ? $input : '',
'documentLoader' => $jsonld_default_load_document)); 'compactArrays' => true,
'graph' => false,
if($options['skipExpansion'] === true) { 'skipExpansion' => false,
$expanded = $input; 'activeCtx' => false,
} else { 'documentLoader' => $jsonld_default_load_document,
// expand input 'link' => false]);
try { if ($options['link']) {
$expanded = $this->expand($input, $options); // force skip expansion when linking, "link" is not part of the
} catch(JsonLdException $e) { // public API, it should only be called from framing
throw new JsonLdException( $options['skipExpansion'] = true;
'Could not expand input before compaction.', }
'jsonld.CompactError', null, null, $e);
} if ($options['skipExpansion'] === true) {
} $expanded = $input;
} else {
// process context // expand input
$active_ctx = $this->_getInitialContext($options); try {
try { $expanded = $this->expand($input, $options);
$active_ctx = $this->processContext($active_ctx, $ctx, $options); } catch (JsonLdException $e) {
} catch(JsonLdException $e) { throw new JsonLdException(
throw new JsonLdException( 'Could not expand input before compaction.', 'jsonld.CompactError', null, null, $e);
'Could not process context before compaction.', }
'jsonld.CompactError', null, null, $e); }
}
// process context
// do compaction $active_ctx = $this->_getInitialContext($options);
$compacted = $this->_compact($active_ctx, null, $expanded, $options); try {
$active_ctx = $this->processContext($active_ctx, $ctx, $options);
if($options['compactArrays'] && } catch (JsonLdException $e) {
!$options['graph'] && is_array($compacted)) { throw new JsonLdException(
if(count($compacted) === 1) { 'Could not process context before compaction.', 'jsonld.CompactError', null, null, $e);
// simplify to a single item }
$compacted = $compacted[0];
} else if(count($compacted) === 0) { // do compaction
// simplify to an empty object $compacted = $this->_compact($active_ctx, null, $expanded, $options);
$compacted = new stdClass();
} if ($options['compactArrays'] &&
} else if($options['graph']) { !$options['graph'] && is_array($compacted)) {
// always use array if graph option is on if (count($compacted) === 1) {
$compacted = self::arrayify($compacted); // simplify to a single item
} $compacted = $compacted[0];
} else if (count($compacted) === 0) {
// follow @context key // simplify to an empty object
if(is_object($ctx) && property_exists($ctx, '@context')) { $compacted = new stdClass();
$ctx = $ctx->{'@context'}; }
} } else if ($options['graph']) {
// always use array if graph option is on
// build output context $compacted = self::arrayify($compacted);
$ctx = self::copy($ctx); }
$ctx = self::arrayify($ctx);
// follow @context key
// remove empty contexts if (is_object($ctx) && property_exists($ctx, '@context')) {
$tmp = $ctx; $ctx = $ctx->{'@context'};
$ctx = array(); }
foreach($tmp as $v) {
if(!is_object($v) || count(array_keys((array)$v)) > 0) { // build output context
$ctx[] = $v; $ctx = self::copy($ctx);
} $ctx = self::arrayify($ctx);
}
// remove empty contexts
// remove array if only one context $tmp = $ctx;
$ctx_length = count($ctx); $ctx = [];
$has_context = ($ctx_length > 0); foreach ($tmp as $v) {
if($ctx_length === 1) { if (!is_object($v) || count(array_keys((array) $v)) > 0) {
$ctx = $ctx[0]; $ctx[] = $v;
} }
}
// add context and/or @graph
if(is_array($compacted)) { // remove array if only one context
// use '@graph' keyword $ctx_length = count($ctx);
$kwgraph = $this->_compactIri($active_ctx, '@graph'); $has_context = ($ctx_length > 0);
$graph = $compacted; if ($ctx_length === 1) {
$compacted = new stdClass(); $ctx = $ctx[0];
if($has_context) { }
$compacted->{'@context'} = $ctx;
} // add context and/or @graph
$compacted->{$kwgraph} = $graph; if (is_array($compacted)) {
} else if(is_object($compacted) && $has_context) { // use '@graph' keyword
// reorder keys so @context is first $kwgraph = $this->_compactIri($active_ctx, '@graph');
$graph = $compacted; $graph = $compacted;
$compacted = new stdClass(); $compacted = new stdClass();
$compacted->{'@context'} = $ctx; if ($has_context) {
foreach($graph as $k => $v) { $compacted->{'@context'} = $ctx;
$compacted->{$k} = $v; }
} $compacted->{$kwgraph} = $graph;
} } else if (is_object($compacted) && $has_context) {
// reorder keys so @context is first
if($options['activeCtx']) { $graph = $compacted;
return array('compacted' => $compacted, 'activeCtx' => $active_ctx); $compacted = new stdClass();
} $compacted->{'@context'} = $ctx;
foreach ($graph as $k => $v) {
return $compacted; $compacted->{$k} = $v;
} }
}
/**
* Performs JSON-LD expansion. if ($options['activeCtx']) {
* return ['compacted' => $compacted, 'activeCtx' => $active_ctx];
* @param mixed $input the JSON-LD object to expand. }
* @param assoc $options the options to use:
* [base] the base IRI to use. return $compacted;
* [expandContext] a context to expand with. }
* [keepFreeFloatingNodes] true to keep free-floating nodes,
* false not to, defaults to false. /**
* [documentLoader(url)] the document loader. * Performs JSON-LD expansion.
* *
* @return array the expanded JSON-LD output. * @param mixed $input the JSON-LD object to expand.
*/ * @param assoc $options the options to use:
public function expand($input, $options) { * [base] the base IRI to use.
global $jsonld_default_load_document; * [expandContext] a context to expand with.
self::setdefaults($options, array( * [keepFreeFloatingNodes] true to keep free-floating nodes,
'keepFreeFloatingNodes' => false, * false not to, defaults to false.
'documentLoader' => $jsonld_default_load_document)); * [documentLoader(url)] the document loader.
*
// if input is a string, attempt to dereference remote document * @return array the expanded JSON-LD output.
if(is_string($input)) { */
$remote_doc = call_user_func($options['documentLoader'], $input); public function expand($input, $options)
} else { {
$remote_doc = (object)array( global $jsonld_default_load_document;
'contextUrl' => null, self::setdefaults($options, [
'documentUrl' => null, 'keepFreeFloatingNodes' => false,
'document' => $input); 'documentLoader' => $jsonld_default_load_document]);
}
// if input is a string, attempt to dereference remote document
try { if (is_string($input)) {
if($remote_doc->document === null) { $remote_doc = call_user_func($options['documentLoader'], $input);
throw new JsonLdException( } else {
'No remote document found at the given URL.', $remote_doc = (object) [
'jsonld.NullRemoteDocument'); 'contextUrl' => null,
} 'documentUrl' => null,
if(is_string($remote_doc->document)) { 'document' => $input];
$remote_doc->document = self::_parse_json($remote_doc->document); }
}
} catch(Exception $e) { try {
throw new JsonLdException( if ($remote_doc->document === null) {
'Could not retrieve a JSON-LD document from the URL.', throw new JsonLdException(
'jsonld.LoadDocumentError', 'loading document failed', 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument');
array('remoteDoc' => $remote_doc), $e); }
} if (is_string($remote_doc->document)) {
$remote_doc->document = self::_parse_json($remote_doc->document);
// set default base }
self::setdefault($options, 'base', $remote_doc->documentUrl ?: ''); } catch (Exception $e) {
throw new JsonLdException(
// build meta-object and retrieve all @context urls 'Could not retrieve a JSON-LD document from the URL.', 'jsonld.LoadDocumentError', 'loading document failed', ['remoteDoc' => $remote_doc], $e);
$input = (object)array( }
'document' => self::copy($remote_doc->document),
'remoteContext' => (object)array( // set default base
'@context' => $remote_doc->contextUrl)); self::setdefault($options, 'base', $remote_doc->documentUrl ?: '');
if(isset($options['expandContext'])) {
$expand_context = self::copy($options['expandContext']); // build meta-object and retrieve all @context urls
if(is_object($expand_context) && $input = (object) [
property_exists($expand_context, '@context')) { 'document' => self::copy($remote_doc->document),
$input->expandContext = $expand_context; 'remoteContext' => (object) [
} else { '@context' => $remote_doc->contextUrl]];
$input->expandContext = (object)array('@context' => $expand_context); if (isset($options['expandContext'])) {
} $expand_context = self::copy($options['expandContext']);
} if (is_object($expand_context) &&
property_exists($expand_context, '@context')) {
// retrieve all @context URLs in the input $input->expandContext = $expand_context;
try { } else {
$this->_retrieveContextUrls( $input->expandContext = (object) ['@context' => $expand_context];
$input, new stdClass(), $options['documentLoader'], $options['base']); }
} catch(Exception $e) { }
throw new JsonLdException(
'Could not perform JSON-LD expansion.', // retrieve all @context URLs in the input
'jsonld.ExpandError', null, null, $e); try {
} $this->_retrieveContextUrls(
$input, new stdClass(), $options['documentLoader'], $options['base']);
$active_ctx = $this->_getInitialContext($options); } catch (Exception $e) {
$document = $input->document; throw new JsonLdException(
$remote_context = $input->remoteContext->{'@context'}; 'Could not perform JSON-LD expansion.', 'jsonld.ExpandError', null, null, $e);
}
// process optional expandContext
if(property_exists($input, 'expandContext')) { $active_ctx = $this->_getInitialContext($options);
$active_ctx = self::_processContext( $document = $input->document;
$active_ctx, $input->expandContext, $options); $remote_context = $input->remoteContext->{'@context'};
}
// process optional expandContext
// process remote context from HTTP Link Header if (property_exists($input, 'expandContext')) {
if($remote_context) { $active_ctx = self::_processContext(
$active_ctx = self::_processContext( $active_ctx, $input->expandContext, $options);
$active_ctx, $remote_context, $options); }
}
// process remote context from HTTP Link Header
// do expansion if ($remote_context) {
$expanded = $this->_expand($active_ctx, null, $document, $options, false); $active_ctx = self::_processContext(
$active_ctx, $remote_context, $options);
// optimize away @graph with no other properties }
if(is_object($expanded) && property_exists($expanded, '@graph') &&
count(array_keys((array)$expanded)) === 1) { // do expansion
$expanded = $expanded->{'@graph'}; $expanded = $this->_expand($active_ctx, null, $document, $options, false);
} else if($expanded === null) {
$expanded = array(); // optimize away @graph with no other properties
} if (is_object($expanded) && property_exists($expanded, '@graph') &&
// normalize to an array count(array_keys((array) $expanded)) === 1) {
return self::arrayify($expanded); $expanded = $expanded->{'@graph'};
} } else if ($expanded === null) {
$expanded = [];
/** }
* Performs JSON-LD flattening. // normalize to an array
* return self::arrayify($expanded);
* @param mixed $input the JSON-LD to flatten. }
* @param ctx the context to use to compact the flattened output, or null.
* @param assoc $options the options to use: /**
* [base] the base IRI to use. * Performs JSON-LD flattening.
* [expandContext] a context to expand with. *
* [documentLoader(url)] the document loader. * @param mixed $input the JSON-LD to flatten.
* * @param ctx the context to use to compact the flattened output, or null.
* @return array the flattened output. * @param assoc $options the options to use:
*/ * [base] the base IRI to use.
public function flatten($input, $ctx, $options) { * [expandContext] a context to expand with.
global $jsonld_default_load_document; * [documentLoader(url)] the document loader.
self::setdefaults($options, array( *
'base' => is_string($input) ? $input : '', * @return array the flattened output.
'documentLoader' => $jsonld_default_load_document)); */
public function flatten($input, $ctx, $options)
try { {
// expand input global $jsonld_default_load_document;
$expanded = $this->expand($input, $options); self::setdefaults($options, [
} catch(Exception $e) { 'base' => is_string($input) ? $input : '',
throw new JsonLdException( 'documentLoader' => $jsonld_default_load_document]);
'Could not expand input before flattening.',
'jsonld.FlattenError', null, null, $e); try {
} // expand input
$expanded = $this->expand($input, $options);
// do flattening } catch (Exception $e) {
$flattened = $this->_flatten($expanded); throw new JsonLdException(
'Could not expand input before flattening.', 'jsonld.FlattenError', null, null, $e);
if($ctx === null) { }
return $flattened;
} // do flattening
$flattened = $this->_flatten($expanded);
// compact result (force @graph option to true, skip expansion)
$options['graph'] = true; if ($ctx === null) {
$options['skipExpansion'] = true; return $flattened;
try { }
$compacted = $this->compact($flattened, $ctx, $options);
} catch(Exception $e) { // compact result (force @graph option to true, skip expansion)
throw new JsonLdException( $options['graph'] = true;
'Could not compact flattened output.', $options['skipExpansion'] = true;
'jsonld.FlattenError', null, null, $e); try {
} $compacted = $this->compact($flattened, $ctx, $options);
} catch (Exception $e) {
return $compacted; throw new JsonLdException(
} 'Could not compact flattened output.', 'jsonld.FlattenError', null, null, $e);
}
/**
* Performs JSON-LD framing. return $compacted;
* }
* @param mixed $input the JSON-LD object to frame.
* @param stdClass $frame the JSON-LD frame to use. /**
* @param $options the framing options. * Performs JSON-LD framing.
* [base] the base IRI to use. *
* [expandContext] a context to expand with. * @param mixed $input the JSON-LD object to frame.
* [embed] default @embed flag (default: true). * @param stdClass $frame the JSON-LD frame to use.
* [explicit] default @explicit flag (default: false). * @param $options the framing options.
* [omitDefault] default @omitDefault flag (default: false). * [base] the base IRI to use.
* [documentLoader(url)] the document loader. * [expandContext] a context to expand with.
* * [embed] default @embed flag: '@last', '@always', '@never', '@link'
* @return stdClass the framed JSON-LD output. * (default: '@last').
*/ * [explicit] default @explicit flag (default: false).
public function frame($input, $frame, $options) { * [requireAll] default @requireAll flag (default: true).
global $jsonld_default_load_document; * [omitDefault] default @omitDefault flag (default: false).
self::setdefaults($options, array( * [documentLoader(url)] the document loader.
'base' => is_string($input) ? $input : '', *
'compactArrays' => true, * @return stdClass the framed JSON-LD output.
'embed' => true, */
'explicit' => false, public function frame($input, $frame, $options)
'omitDefault' => false, {
'documentLoader' => $jsonld_default_load_document)); global $jsonld_default_load_document;
self::setdefaults($options, [
// if frame is a string, attempt to dereference remote document 'base' => is_string($input) ? $input : '',
if(is_string($frame)) { 'compactArrays' => true,
$remote_frame = call_user_func($options['documentLoader'], $frame); 'embed' => '@last',
} else { 'explicit' => false,
$remote_frame = (object)array( 'requireAll' => true,
'contextUrl' => null, 'omitDefault' => false,
'documentUrl' => null, 'documentLoader' => $jsonld_default_load_document]);
'document' => $frame);
} // if frame is a string, attempt to dereference remote document
if (is_string($frame)) {
try { $remote_frame = call_user_func($options['documentLoader'], $frame);
if($remote_frame->document === null) { } else {
throw new JsonLdException( $remote_frame = (object) [
'No remote document found at the given URL.', 'contextUrl' => null,
'jsonld.NullRemoteDocument'); 'documentUrl' => null,
} 'document' => $frame];
if(is_string($remote_frame->document)) { }
$remote_frame->document = self::_parse_json($remote_frame->document);
} try {
} catch(Exception $e) { if ($remote_frame->document === null) {
throw new JsonLdException( throw new JsonLdException(
'Could not retrieve a JSON-LD document from the URL.', 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument');
'jsonld.LoadDocumentError', 'loading document failed', }
array('remoteDoc' => $remote_frame), $e); if (is_string($remote_frame->document)) {
} $remote_frame->document = self::_parse_json($remote_frame->document);
}
// preserve frame context } catch (Exception $e) {
$frame = $remote_frame->document; throw new JsonLdException(
if($frame !== null) { 'Could not retrieve a JSON-LD document from the URL.', 'jsonld.LoadDocumentError', 'loading document failed', ['remoteDoc' => $remote_frame], $e);
$ctx = (property_exists($frame, '@context') ? }
$frame->{'@context'} : new stdClass());
if($remote_frame->contextUrl !== null) { // preserve frame context
if($ctx !== null) { $frame = $remote_frame->document;
$ctx = $remote_frame->contextUrl; if ($frame !== null) {
} else { $ctx = (property_exists($frame, '@context') ?
$ctx = self::arrayify($ctx); $frame->{'@context'} : new stdClass());
$ctx[] = $remote_frame->contextUrl; if ($remote_frame->contextUrl !== null) {
} if ($ctx !== null) {
$frame->{'@context'} = $ctx; $ctx = $remote_frame->contextUrl;
} } else {
} $ctx = self::arrayify($ctx);
$ctx[] = $remote_frame->contextUrl;
try { }
// expand input $frame->{'@context'} = $ctx;
$expanded = $this->expand($input, $options); }
} catch(Exception $e) { }
throw new JsonLdException(
'Could not expand input before framing.', try {
'jsonld.FrameError', null, null, $e); // expand input
} $expanded = $this->expand($input, $options);
} catch (Exception $e) {
try { throw new JsonLdException(
// expand frame 'Could not expand input before framing.', 'jsonld.FrameError', null, null, $e);
$opts = $options; }
$opts['keepFreeFloatingNodes'] = true;
$expanded_frame = $this->expand($frame, $opts); try {
} catch(Exception $e) { // expand frame
throw new JsonLdException( $opts = $options;
'Could not expand frame before framing.', $opts['keepFreeFloatingNodes'] = true;
'jsonld.FrameError', null, null, $e); $expanded_frame = $this->expand($frame, $opts);
} } catch (Exception $e) {
throw new JsonLdException(
// do framing 'Could not expand frame before framing.', 'jsonld.FrameError', null, null, $e);
$framed = $this->_frame($expanded, $expanded_frame, $options); }
try { // do framing
// compact result (force @graph option to true) $framed = $this->_frame($expanded, $expanded_frame, $options);
$options['graph'] = true;
$options['skipExpansion'] = true; try {
$options['activeCtx'] = true; // compact result (force @graph option to true, skip expansion, check
$result = $this->compact($framed, $ctx, $options); // for linked embeds)
} catch(Exception $e) { $options['graph'] = true;
throw new JsonLdException( $options['skipExpansion'] = true;
'Could not compact framed output.', $options['link'] = new ArrayObject();
'jsonld.FrameError', null, null, $e); $options['activeCtx'] = true;
} $result = $this->compact($framed, $ctx, $options);
} catch (Exception $e) {
$compacted = $result['compacted']; throw new JsonLdException(
$active_ctx = $result['activeCtx']; 'Could not compact framed output.', 'jsonld.FrameError', null, null, $e);
}
// get graph alias
$graph = $this->_compactIri($active_ctx, '@graph'); $compacted = $result['compacted'];
// remove @preserve from results $active_ctx = $result['activeCtx'];
$compacted->{$graph} = $this->_removePreserve(
$active_ctx, $compacted->{$graph}, $options); // get graph alias
return $compacted; $graph = $this->_compactIri($active_ctx, '@graph');
} // remove @preserve from results
$options['link'] = new ArrayObject();
/** $compacted->{$graph} = $this->_removePreserve(
* Performs JSON-LD normalization. $active_ctx, $compacted->{$graph}, $options);
* return $compacted;
* @param mixed $input the JSON-LD object to normalize. }
* @param assoc $options the options to use:
* [base] the base IRI to use. /**
* [expandContext] a context to expand with. * Performs JSON-LD normalization.
* [format] the format if output is a string: *
* 'application/nquads' for N-Quads. * @param mixed $input the JSON-LD object to normalize.
* [documentLoader(url)] the document loader. * @param assoc $options the options to use:
* * [base] the base IRI to use.
* @return mixed the normalized output. * [expandContext] a context to expand with.
*/ * [inputFormat] the format if input is not JSON-LD:
public function normalize($input, $options) { * 'application/nquads' for N-Quads.
global $jsonld_default_load_document; * [format] the format if output is a string:
self::setdefaults($options, array( * 'application/nquads' for N-Quads.
'base' => is_string($input) ? $input : '', * [documentLoader(url)] the document loader.
'documentLoader' => $jsonld_default_load_document)); *
* @return mixed the normalized output.
try { */
// convert to RDF dataset then do normalization public function normalize($input, $options)
$opts = $options; {
if(isset($opts['format'])) { global $jsonld_default_load_document;
unset($opts['format']); self::setdefaults($options, [
} 'base' => is_string($input) ? $input : '',
$opts['produceGeneralizedRdf'] = false; 'documentLoader' => $jsonld_default_load_document]);
$dataset = $this->toRDF($input, $opts);
} catch(Exception $e) { if (isset($options['inputFormat'])) {
throw new JsonLdException( if ($options['inputFormat'] != 'application/nquads') {
'Could not convert input to RDF dataset before normalization.', throw new JsonLdException(
'jsonld.NormalizeError', null, null, $e); 'Unknown normalization input format.', 'jsonld.NormalizeError');
} }
$dataset = $this->parseNQuads($input);
// do normalization } else {
return $this->_normalize($dataset, $options); try {
} // convert to RDF dataset then do normalization
$opts = $options;
/** if (isset($opts['format'])) {
* Converts an RDF dataset to JSON-LD. unset($opts['format']);
* }
* @param mixed $dataset a serialized string of RDF in a format specified $opts['produceGeneralizedRdf'] = false;
* by the format option or an RDF dataset to convert. $dataset = $this->toRDF($input, $opts);
* @param assoc $options the options to use: } catch (Exception $e) {
* [format] the format if input is a string: throw new JsonLdException(
* 'application/nquads' for N-Quads (default). 'Could not convert input to RDF dataset before normalization.', 'jsonld.NormalizeError', null, null, $e);
* [useRdfType] true to use rdf:type, false to use @type }
* (default: false). }
* [useNativeTypes] true to convert XSD types into native types
* (boolean, integer, double), false not to (default: false). // do normalization
* return $this->_normalize($dataset, $options);
* @return array the JSON-LD output. }
*/
public function fromRDF($dataset, $options) { /**
global $jsonld_rdf_parsers; * Converts an RDF dataset to JSON-LD.
*
self::setdefaults($options, array( * @param mixed $dataset a serialized string of RDF in a format specified
'useRdfType' => false, * by the format option or an RDF dataset to convert.
'useNativeTypes' => false)); * @param assoc $options the options to use:
* [format] the format if input is a string:
if(!isset($options['format']) && is_string($dataset)) { * 'application/nquads' for N-Quads (default).
// set default format to nquads * [useRdfType] true to use rdf:type, false to use @type
$options['format'] = 'application/nquads'; * (default: false).
} * [useNativeTypes] true to convert XSD types into native types
* (boolean, integer, double), false not to (default: false).
// handle special format *
if(isset($options['format']) && $options['format']) { * @return array the JSON-LD output.
// supported formats (processor-specific and global) */
if(($this->rdfParsers !== null && public function fromRDF($dataset, $options)
!property_exists($this->rdfParsers, $options['format'])) || {
$this->rdfParsers === null && global $jsonld_rdf_parsers;
!property_exists($jsonld_rdf_parsers, $options['format'])) {
throw new JsonLdException( self::setdefaults($options, [
'Unknown input format.', 'useRdfType' => false,
'jsonld.UnknownFormat', null, array('format' => $options['format'])); 'useNativeTypes' => false]);
}
if($this->rdfParsers !== null) { if (!isset($options['format']) && is_string($dataset)) {
$callable = $this->rdfParsers->{$options['format']}; // set default format to nquads
} else { $options['format'] = 'application/nquads';
$callable = $jsonld_rdf_parsers->{$options['format']}; }
}
$dataset = call_user_func($callable, $dataset); // handle special format
} if (isset($options['format']) && $options['format']) {
// supported formats (processor-specific and global)
// convert from RDF if (($this->rdfParsers !== null &&
return $this->_fromRDF($dataset, $options); !property_exists($this->rdfParsers, $options['format'])) ||
} $this->rdfParsers === null &&
!property_exists($jsonld_rdf_parsers, $options['format'])) {
/** throw new JsonLdException(
* Outputs the RDF dataset found in the given JSON-LD object. 'Unknown input format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]);
* }
* @param mixed $input the JSON-LD object. if ($this->rdfParsers !== null) {
* @param assoc $options the options to use: $callable = $this->rdfParsers->{$options['format']};
* [base] the base IRI to use. } else {
* [expandContext] a context to expand with. $callable = $jsonld_rdf_parsers->{$options['format']};
* [format] the format to use to output a string: }
* 'application/nquads' for N-Quads. $dataset = call_user_func($callable, $dataset);
* [produceGeneralizedRdf] true to output generalized RDF, false }
* to produce only standard RDF (default: false).
* [documentLoader(url)] the document loader. // convert from RDF
* return $this->_fromRDF($dataset, $options);
* @return mixed the resulting RDF dataset (or a serialization of it). }
*/
public function toRDF($input, $options) { /**
global $jsonld_default_load_document; * Outputs the RDF dataset found in the given JSON-LD object.
self::setdefaults($options, array( *
'base' => is_string($input) ? $input : '', * @param mixed $input the JSON-LD object.
'produceGeneralizedRdf' => false, * @param assoc $options the options to use:
'documentLoader' => $jsonld_default_load_document)); * [base] the base IRI to use.
* [expandContext] a context to expand with.
try { * [format] the format to use to output a string:
// expand input * 'application/nquads' for N-Quads.
$expanded = $this->expand($input, $options); * [produceGeneralizedRdf] true to output generalized RDF, false
} catch(JsonLdException $e) { * to produce only standard RDF (default: false).
throw new JsonLdException( * [documentLoader(url)] the document loader.
'Could not expand input before serialization to RDF.', *
'jsonld.RdfError', null, null, $e); * @return mixed the resulting RDF dataset (or a serialization of it).
} */
public function toRDF($input, $options)
// create node map for default graph (and any named graphs) {
$namer = new UniqueNamer('_:b'); global $jsonld_default_load_document;
$node_map = (object)array('@default' => new stdClass()); self::setdefaults($options, [
$this->_createNodeMap($expanded, $node_map, '@default', $namer); 'base' => is_string($input) ? $input : '',
'produceGeneralizedRdf' => false,
// output RDF dataset 'documentLoader' => $jsonld_default_load_document]);
$dataset = new stdClass();
$graph_names = array_keys((array)$node_map); try {
sort($graph_names); // expand input
foreach($graph_names as $graph_name) { $expanded = $this->expand($input, $options);
$graph = $node_map->{$graph_name}; } catch (JsonLdException $e) {
// skip relative IRIs throw new JsonLdException(
if($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) { 'Could not expand input before serialization to RDF.', 'jsonld.RdfError', null, null, $e);
$dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options); }
}
} // create node map for default graph (and any named graphs)
$namer = new UniqueNamer('_:b');
$rval = $dataset; $node_map = (object) ['@default' => new stdClass()];
$this->_createNodeMap($expanded, $node_map, '@default', $namer);
// convert to output format
if(isset($options['format']) && $options['format']) { // output RDF dataset
// supported formats $dataset = new stdClass();
if($options['format'] === 'application/nquads') { $graph_names = array_keys((array) $node_map);
$rval = self::toNQuads($dataset); sort($graph_names);
} else { foreach ($graph_names as $graph_name) {
throw new JsonLdException( $graph = $node_map->{$graph_name};
'Unknown output format.', 'jsonld.UnknownFormat', // skip relative IRIs
null, array('format' => $options['format'])); if ($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) {
} $dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options);
} }
}
return $rval;
} $rval = $dataset;
/** // convert to output format
* Processes a local context, resolving any URLs as necessary, and returns a if (isset($options['format']) && $options['format']) {
* new active context in its callback. // supported formats
* if ($options['format'] === 'application/nquads') {
* @param stdClass $active_ctx the current active context. $rval = self::toNQuads($dataset);
* @param mixed $local_ctx the local context to process. } else {
* @param assoc $options the options to use: throw new JsonLdException(
* [documentLoader(url)] the document loader. 'Unknown output format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]);
* }
* @return stdClass the new active context. }
*/
public function processContext($active_ctx, $local_ctx, $options) { return $rval;
global $jsonld_default_load_document; }
self::setdefaults($options, array(
'base' => '', /**
'documentLoader' => $jsonld_default_load_document)); * Processes a local context, resolving any URLs as necessary, and returns a
* new active context in its callback.
// return initial context early for null context *
if($local_ctx === null) { * @param stdClass $active_ctx the current active context.
return $this->_getInitialContext($options); * @param mixed $local_ctx the local context to process.
} * @param assoc $options the options to use:
* [documentLoader(url)] the document loader.
// retrieve URLs in local_ctx *
$local_ctx = self::copy($local_ctx); * @return stdClass the new active context.
if(is_string($local_ctx) or ( */
is_object($local_ctx) && !property_exists($local_ctx, '@context'))) { public function processContext($active_ctx, $local_ctx, $options)
$local_ctx = (object)array('@context' => $local_ctx); {
} global $jsonld_default_load_document;
try { self::setdefaults($options, [
$this->_retrieveContextUrls( 'base' => '',
$local_ctx, new stdClass(), 'documentLoader' => $jsonld_default_load_document]);
$options['documentLoader'], $options['base']);
} catch(Exception $e) { // return initial context early for null context
throw new JsonLdException( if ($local_ctx === null) {
'Could not process JSON-LD context.', return $this->_getInitialContext($options);
'jsonld.ContextError', null, null, $e); }
}
// retrieve URLs in local_ctx
// process context $local_ctx = self::copy($local_ctx);
return $this->_processContext($active_ctx, $local_ctx, $options); if (is_string($local_ctx) or (
} is_object($local_ctx) && !property_exists($local_ctx, '@context'))) {
$local_ctx = (object) ['@context' => $local_ctx];
/** }
* Returns true if the given subject has the given property. try {
* $this->_retrieveContextUrls(
* @param stdClass $subject the subject to check. $local_ctx, new stdClass(), $options['documentLoader'], $options['base']);
* @param string $property the property to look for. } catch (Exception $e) {
* throw new JsonLdException(
* @return bool true if the subject has the given property, false if not. 'Could not process JSON-LD context.', 'jsonld.ContextError', null, null, $e);
*/ }
public static function hasProperty($subject, $property) {
$rval = false; // process context
if(property_exists($subject, $property)) { return $this->_processContext($active_ctx, $local_ctx, $options);
$value = $subject->{$property}; }
$rval = (!is_array($value) || count($value) > 0);
} /**
return $rval; * Returns true if the given subject has the given property.
} *
* @param stdClass $subject the subject to check.
/** * @param string $property the property to look for.
* Determines if the given value is a property of the given subject. *
* * @return bool true if the subject has the given property, false if not.
* @param stdClass $subject the subject to check. */
* @param string $property the property to check. public static function hasProperty($subject, $property)
* @param mixed $value the value to check. {
* $rval = false;
* @return bool true if the value exists, false if not. if (property_exists($subject, $property)) {
*/ $value = $subject->{$property};
public static function hasValue($subject, $property, $value) { $rval = (!is_array($value) || count($value) > 0);
$rval = false; }
if(self::hasProperty($subject, $property)) { return $rval;
$val = $subject->{$property}; }
$is_list = self::_isList($val);
if(is_array($val) || $is_list) { /**
if($is_list) { * Determines if the given value is a property of the given subject.
$val = $val->{'@list'}; *
} * @param stdClass $subject the subject to check.
foreach($val as $v) { * @param string $property the property to check.
if(self::compareValues($value, $v)) { * @param mixed $value the value to check.
$rval = true; *
break; * @return bool true if the value exists, false if not.
} */
} public static function hasValue($subject, $property, $value)
} else if(!is_array($value)) { {
// avoid matching the set of values with an array value parameter $rval = false;
$rval = self::compareValues($value, $val); if (self::hasProperty($subject, $property)) {
} $val = $subject->{$property};
} $is_list = self::_isList($val);
return $rval; if (is_array($val) || $is_list) {
} if ($is_list) {
$val = $val->{'@list'};
/** }
* Adds a value to a subject. If the value is an array, all values in the foreach ($val as $v) {
* array will be added. if (self::compareValues($value, $v)) {
* $rval = true;
* Note: If the value is a subject that already exists as a property of the break;
* given subject, this method makes no attempt to deeply merge properties. }
* Instead, the value will not be added. }
* } else if (!is_array($value)) {
* @param stdClass $subject the subject to add the value to. // avoid matching the set of values with an array value parameter
* @param string $property the property that relates the value to the subject. $rval = self::compareValues($value, $val);
* @param mixed $value the value to add. }
* @param assoc [$options] the options to use: }
* [propertyIsArray] true if the property is always an array, false return $rval;
* if not (default: false). }
* [allowDuplicate] true to allow duplicates, false not to (uses a
* simple shallow comparison of subject ID or value) /**
* (default: true). * Adds a value to a subject. If the value is an array, all values in the
*/ * array will be added.
public static function addValue( *
$subject, $property, $value, $options=array()) { * Note: If the value is a subject that already exists as a property of the
self::setdefaults($options, array( * given subject, this method makes no attempt to deeply merge properties.
'allowDuplicate' => true, * Instead, the value will not be added.
'propertyIsArray' => false)); *
* @param stdClass $subject the subject to add the value to.
if(is_array($value)) { * @param string $property the property that relates the value to the subject.
if(count($value) === 0 && $options['propertyIsArray'] && * @param mixed $value the value to add.
!property_exists($subject, $property)) { * @param assoc [$options] the options to use:
$subject->{$property} = array(); * [propertyIsArray] true if the property is always an array, false
} * if not (default: false).
foreach($value as $v) { * [allowDuplicate] true to allow duplicates, false not to (uses a
self::addValue($subject, $property, $v, $options); * simple shallow comparison of subject ID or value)
} * (default: true).
} else if(property_exists($subject, $property)) { */
// check if subject already has value if duplicates not allowed public static function addValue(
$has_value = (!$options['allowDuplicate'] && $subject, $property, $value, $options = [])
self::hasValue($subject, $property, $value)); {
self::setdefaults($options, [
// make property an array if value not present or always an array 'allowDuplicate' => true,
if(!is_array($subject->{$property}) && 'propertyIsArray' => false]);
(!$has_value || $options['propertyIsArray'])) {
$subject->{$property} = array($subject->{$property}); if (is_array($value)) {
} if (count($value) === 0 && $options['propertyIsArray'] &&
!property_exists($subject, $property)) {
// add new value $subject->{$property} = [];
if(!$has_value) { }
$subject->{$property}[] = $value; foreach ($value as $v) {
} self::addValue($subject, $property, $v, $options);
} else { }
// add new value as set or single value } else if (property_exists($subject, $property)) {
$subject->{$property} = ($options['propertyIsArray'] ? // check if subject already has value if duplicates not allowed
array($value) : $value); $has_value = (!$options['allowDuplicate'] &&
} self::hasValue($subject, $property, $value));
}
// make property an array if value not present or always an array
/** if (!is_array($subject->{$property}) &&
* Gets all of the values for a subject's property as an array. (!$has_value || $options['propertyIsArray'])) {
* $subject->{$property} = [$subject->{$property}];
* @param stdClass $subject the subject. }
* @param string $property the property.
* // add new value
* @return array all of the values for a subject's property as an array. if (!$has_value) {
*/ $subject->{$property}[] = $value;
public static function getValues($subject, $property) { }
$rval = (property_exists($subject, $property) ? } else {
$subject->{$property} : array()); // add new value as set or single value
return self::arrayify($rval); $subject->{$property} = ($options['propertyIsArray'] ?
} [$value] : $value);
}
/** }
* Removes a property from a subject.
* /**
* @param stdClass $subject the subject. * Gets all of the values for a subject's property as an array.
* @param string $property the property. *
*/ * @param stdClass $subject the subject.
public static function removeProperty($subject, $property) { * @param string $property the property.
unset($subject->{$property}); *
} * @return array all of the values for a subject's property as an array.
*/
/** public static function getValues($subject, $property)
* Removes a value from a subject. {
* $rval = (property_exists($subject, $property) ?
* @param stdClass $subject the subject. $subject->{$property} : []);
* @param string $property the property that relates the value to the subject. return self::arrayify($rval);
* @param mixed $value the value to remove. }
* @param assoc [$options] the options to use:
* [propertyIsArray] true if the property is always an array, /**
* false if not (default: false). * Removes a property from a subject.
*/ *
public static function removeValue( * @param stdClass $subject the subject.
$subject, $property, $value, $options=array()) { * @param string $property the property.
self::setdefaults($options, array( */
'propertyIsArray' => false)); public static function removeProperty($subject, $property)
{
// filter out value unset($subject->{$property});
$filter = function($e) use ($value) { }
return !self::compareValues($e, $value);
}; /**
$values = self::getValues($subject, $property); * Removes a value from a subject.
$values = array_values(array_filter($values, $filter)); *
* @param stdClass $subject the subject.
if(count($values) === 0) { * @param string $property the property that relates the value to the subject.
self::removeProperty($subject, $property); * @param mixed $value the value to remove.
} else if(count($values) === 1 && !$options['propertyIsArray']) { * @param assoc [$options] the options to use:
$subject->{$property} = $values[0]; * [propertyIsArray] true if the property is always an array,
} else { * false if not (default: false).
$subject->{$property} = $values; */
} public static function removeValue(
} $subject, $property, $value, $options = [])
{
/** self::setdefaults($options, [
* Compares two JSON-LD values for equality. Two JSON-LD values will be 'propertyIsArray' => false]);
* considered equal if:
* // filter out value
* 1. They are both primitives of the same type and value. $filter = function($e) use ($value) {
* 2. They are both @values with the same @value, @type, @language, return !self::compareValues($e, $value);
* and @index, OR };
* 3. They both have @ids that are the same. $values = self::getValues($subject, $property);
* $values = array_values(array_filter($values, $filter));
* @param mixed $v1 the first value.
* @param mixed $v2 the second value. if (count($values) === 0) {
* self::removeProperty($subject, $property);
* @return bool true if v1 and v2 are considered equal, false if not. } else if (count($values) === 1 && !$options['propertyIsArray']) {
*/ $subject->{$property} = $values[0];
public static function compareValues($v1, $v2) { } else {
// 1. equal primitives $subject->{$property} = $values;
if($v1 === $v2) { }
return true; }
}
/**
// 2. equal @values * Compares two JSON-LD values for equality. Two JSON-LD values will be
if(self::_isValue($v1) && self::_isValue($v2)) { * considered equal if:
return ( *
self::_compareKeyValues($v1, $v2, '@value') && * 1. They are both primitives of the same type and value.
self::_compareKeyValues($v1, $v2, '@type') && * 2. They are both @values with the same @value, @type, @language,
self::_compareKeyValues($v1, $v2, '@language') && * and @index, OR
self::_compareKeyValues($v1, $v2, '@index')); * 3. They both have @ids that are the same.
} *
* @param mixed $v1 the first value.
// 3. equal @ids * @param mixed $v2 the second value.
if(is_object($v1) && property_exists($v1, '@id') && *
is_object($v2) && property_exists($v2, '@id')) { * @return bool true if v1 and v2 are considered equal, false if not.
return $v1->{'@id'} === $v2->{'@id'}; */
} public static function compareValues($v1, $v2)
{
return false; // 1. equal primitives
} if ($v1 === $v2) {
return true;
/** }
* Gets the value for the given active context key and type, null if none is
* set. // 2. equal @values
* if (self::_isValue($v1) && self::_isValue($v2)) {
* @param stdClass $ctx the active context. return (
* @param string $key the context key. self::_compareKeyValues($v1, $v2, '@value') &&
* @param string [$type] the type of value to get (eg: '@id', '@type'), if not self::_compareKeyValues($v1, $v2, '@type') &&
* specified gets the entire entry for a key, null if not found. self::_compareKeyValues($v1, $v2, '@language') &&
* self::_compareKeyValues($v1, $v2, '@index'));
* @return mixed the value. }
*/
public static function getContextValue($ctx, $key, $type) { // 3. equal @ids
$rval = null; if (is_object($v1) && property_exists($v1, '@id') &&
is_object($v2) && property_exists($v2, '@id')) {
// return null for invalid key return $v1->{'@id'} === $v2->{'@id'};
if($key === null) { }
return $rval;
} return false;
}
// get default language
if($type === '@language' && property_exists($ctx, $type)) { /**
$rval = $ctx->{$type}; * Gets the value for the given active context key and type, null if none is
} * set.
*
// get specific entry information * @param stdClass $ctx the active context.
if(property_exists($ctx->mappings, $key)) { * @param string $key the context key.
$entry = $ctx->mappings->{$key}; * @param string [$type] the type of value to get (eg: '@id', '@type'), if not
if($entry === null) { * specified gets the entire entry for a key, null if not found.
return null; *
} * @return mixed the value.
*/
if($type === null) { public static function getContextValue($ctx, $key, $type)
// return whole entry {
$rval = $entry; $rval = null;
} else if(property_exists($entry, $type)) {
// return entry value for type // return null for invalid key
$rval = $entry->{$type}; if ($key === null) {
} return $rval;
} }
return $rval; // get default language
} if ($type === '@language' && property_exists($ctx, $type)) {
$rval = $ctx->{$type};
/** }
* Parses RDF in the form of N-Quads.
* // get specific entry information
* @param string $input the N-Quads input to parse. if (property_exists($ctx->mappings, $key)) {
* $entry = $ctx->mappings->{$key};
* @return stdClass an RDF dataset. if ($entry === null) {
*/ return null;
public static function parseNQuads($input) { }
// define partial regexes
$iri = '(?:<([^:]+:[^>]*)>)'; if ($type === null) {
$bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))'; // return whole entry
$plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"'; $rval = $entry;
$datatype = "(?:\\^\\^$iri)"; } else if (property_exists($entry, $type)) {
$language = '(?:@([a-z]+(?:-[a-z0-9]+)*))'; // return entry value for type
$literal = "(?:$plain(?:$datatype|$language)?)"; $rval = $entry->{$type};
$ws = '[ \\t]'; }
$eoln = '/(?:\r\n)|(?:\n)|(?:\r)/'; }
$empty = "/^$ws*$/";
return $rval;
// define quad part regexes }
$subject = "(?:$iri|$bnode)$ws+";
$property = "$iri$ws+"; /**
$object = "(?:$iri|$bnode|$literal)$ws*"; * Parses RDF in the form of N-Quads.
$graph_name = "(?:\\.|(?:(?:$iri|$bnode)$ws*\\.))"; *
* @param string $input the N-Quads input to parse.
// full quad regex *
$quad = "/^$ws*$subject$property$object$graph_name$ws*$/"; * @return stdClass an RDF dataset.
*/
// build RDF dataset public static function parseNQuads($input)
$dataset = new stdClass(); {
// define partial regexes
// split N-Quad input into lines $iri = '(?:<([^:]+:[^>]*)>)';
$lines = preg_split($eoln, $input); $bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))';
$line_number = 0; $plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"';
foreach($lines as $line) { $datatype = "(?:\\^\\^$iri)";
$line_number += 1; $language = '(?:@([a-z]+(?:-[a-z0-9]+)*))';
$literal = "(?:$plain(?:$datatype|$language)?)";
// skip empty lines $ws = '[ \\t]';
if(preg_match($empty, $line)) { $eoln = '/(?:\r\n)|(?:\n)|(?:\r)/';
continue; $empty = "/^$ws*$/";
}
// define quad part regexes
// parse quad $subject = "(?:$iri|$bnode)$ws+";
if(!preg_match($quad, $line, $match)) { $property = "$iri$ws+";
throw new JsonLdException( $object = "(?:$iri|$bnode|$literal)$ws*";
'Error while parsing N-Quads; invalid quad.', $graph_name = "(?:\\.|(?:(?:$iri|$bnode)$ws*\\.))";
'jsonld.ParseError', null, array('line' => $line_number));
} // full quad regex
$quad = "/^$ws*$subject$property$object$graph_name$ws*$/";
// create RDF triple
$triple = (object)array( // build RDF dataset
'subject' => new stdClass(), $dataset = new stdClass();
'predicate' => new stdClass(),
'object' => new stdClass()); // split N-Quad input into lines
$lines = preg_split($eoln, $input);
// get subject $line_number = 0;
if($match[1] !== '') { foreach ($lines as $line) {
$triple->subject->type = 'IRI'; $line_number += 1;
$triple->subject->value = $match[1];
} else { // skip empty lines
$triple->subject->type = 'blank node'; if (preg_match($empty, $line)) {
$triple->subject->value = $match[2]; continue;
} }
// get predicate // parse quad
$triple->predicate->type = 'IRI'; if (!preg_match($quad, $line, $match)) {
$triple->predicate->value = $match[3]; throw new JsonLdException(
'Error while parsing N-Quads; invalid quad.', 'jsonld.ParseError', null, ['line' => $line_number]);
// get object }
if($match[4] !== '') {
$triple->object->type = 'IRI'; // create RDF triple
$triple->object->value = $match[4]; $triple = (object) [
} else if($match[5] !== '') { 'subject' => new stdClass(),
$triple->object->type = 'blank node'; 'predicate' => new stdClass(),
$triple->object->value = $match[5]; 'object' => new stdClass()];
} else {
$triple->object->type = 'literal'; // get subject
$unescaped = str_replace( if ($match[1] !== '') {
array('\"', '\t', '\n', '\r', '\\\\'), $triple->subject->type = 'IRI';
array('"', "\t", "\n", "\r", '\\'), $triple->subject->value = $match[1];
$match[6]); } else {
if(isset($match[7]) && $match[7] !== '') { $triple->subject->type = 'blank node';
$triple->object->datatype = $match[7]; $triple->subject->value = $match[2];
} else if(isset($match[8]) && $match[8] !== '') { }
$triple->object->datatype = self::RDF_LANGSTRING;
$triple->object->language = $match[8]; // get predicate
} else { $triple->predicate->type = 'IRI';
$triple->object->datatype = self::XSD_STRING; $triple->predicate->value = $match[3];
}
$triple->object->value = $unescaped; // get object
} if ($match[4] !== '') {
$triple->object->type = 'IRI';
// get graph name ('@default' is used for the default graph) $triple->object->value = $match[4];
$name = '@default'; } else if ($match[5] !== '') {
if(isset($match[9]) && $match[9] !== '') { $triple->object->type = 'blank node';
$name = $match[9]; $triple->object->value = $match[5];
} else if(isset($match[10]) && $match[10] !== '') { } else {
$name = $match[10]; $triple->object->type = 'literal';
} $unescaped = str_replace(
['\"', '\t', '\n', '\r', '\\\\'], ['"', "\t", "\n", "\r", '\\'], $match[6]);
// initialize graph in dataset if (isset($match[7]) && $match[7] !== '') {
if(!property_exists($dataset, $name)) { $triple->object->datatype = $match[7];
$dataset->{$name} = array($triple); } else if (isset($match[8]) && $match[8] !== '') {
} else { $triple->object->datatype = self::RDF_LANGSTRING;
// add triple if unique to its graph $triple->object->language = $match[8];
$unique = true; } else {
$triples = &$dataset->{$name}; $triple->object->datatype = self::XSD_STRING;
foreach($triples as $t) { }
if(self::_compareRDFTriples($t, $triple)) { $triple->object->value = $unescaped;
$unique = false; }
break;
} // get graph name ('@default' is used for the default graph)
} $name = '@default';
if($unique) { if (isset($match[9]) && $match[9] !== '') {
$triples[] = $triple; $name = $match[9];
} } else if (isset($match[10]) && $match[10] !== '') {
} $name = $match[10];
} }
return $dataset; // initialize graph in dataset
} if (!property_exists($dataset, $name)) {
$dataset->{$name} = [$triple];
/** } else {
* Converts an RDF dataset to N-Quads. // add triple if unique to its graph
* $unique = true;
* @param stdClass $dataset the RDF dataset to convert. $triples = &$dataset->{$name};
* foreach ($triples as $t) {
* @return string the N-Quads string. if (self::_compareRDFTriples($t, $triple)) {
*/ $unique = false;
public static function toNQuads($dataset) { break;
$quads = array(); }
foreach($dataset as $graph_name => $triples) { }
foreach($triples as $triple) { if ($unique) {
if($graph_name === '@default') { $triples[] = $triple;
$graph_name = null; }
} }
$quads[] = self::toNQuad($triple, $graph_name); }
}
} return $dataset;
sort($quads); }
return implode($quads);
} /**
* Converts an RDF dataset to N-Quads.
/** *
* Converts an RDF triple and graph name to an N-Quad string (a single quad). * @param stdClass $dataset the RDF dataset to convert.
* *
* @param stdClass $triple the RDF triple to convert. * @return string the N-Quads string.
* @param mixed $graph_name the name of the graph containing the triple, null */
* for the default graph. public static function toNQuads($dataset)
* @param string $bnode the bnode the quad is mapped to (optional, for {
* use during normalization only). $quads = [];
* foreach ($dataset as $graph_name => $triples) {
* @return string the N-Quad string. foreach ($triples as $triple) {
*/ if ($graph_name === '@default') {
public static function toNQuad($triple, $graph_name, $bnode=null) { $graph_name = null;
$s = $triple->subject; }
$p = $triple->predicate; $quads[] = self::toNQuad($triple, $graph_name);
$o = $triple->object; }
$g = $graph_name; }
sort($quads);
$quad = ''; return implode($quads);
}
// subject is an IRI
if($s->type === 'IRI') { /**
$quad .= "<{$s->value}>"; * Converts an RDF triple and graph name to an N-Quad string (a single quad).
} else if($bnode !== null) { *
// bnode normalization mode * @param stdClass $triple the RDF triple to convert.
$quad .= ($s->value === $bnode) ? '_:a' : '_:z'; * @param mixed $graph_name the name of the graph containing the triple, null
} else { * for the default graph.
// bnode normal mode * @param string $bnode the bnode the quad is mapped to (optional, for
$quad .= $s->value; * use during normalization only).
} *
$quad .= ' '; * @return string the N-Quad string.
*/
// predicate is an IRI public static function toNQuad($triple, $graph_name, $bnode = null)
if($p->type === 'IRI') { {
$quad .= "<{$p->value}>"; $s = $triple->subject;
} else if($bnode !== null) { $p = $triple->predicate;
// FIXME: TBD what to do with bnode predicates during normalization $o = $triple->object;
// bnode normalization mode $g = $graph_name;
$quad .= '_:p';
} else { $quad = '';
// bnode normal mode
$quad .= $p->value; // subject is an IRI
} if ($s->type === 'IRI') {
$quad .= ' '; $quad .= "<{$s->value}>";
} else if ($bnode !== null) {
// object is IRI, bnode, or literal // bnode normalization mode
if($o->type === 'IRI') { $quad .= ($s->value === $bnode) ? '_:a' : '_:z';
$quad .= "<{$o->value}>"; } else {
} else if($o->type === 'blank node') { // bnode normal mode
if($bnode !== null) { $quad .= $s->value;
// normalization mode }
$quad .= ($o->value === $bnode) ? '_:a' : '_:z'; $quad .= ' ';
} else {
// normal mode // predicate is an IRI
$quad .= $o->value; if ($p->type === 'IRI') {
} $quad .= "<{$p->value}>";
} else { } else if ($bnode !== null) {
$escaped = str_replace( // FIXME: TBD what to do with bnode predicates during normalization
array('\\', "\t", "\n", "\r", '"'), // bnode normalization mode
array('\\\\', '\t', '\n', '\r', '\"'), $quad .= '_:p';
$o->value); } else {
$quad .= '"' . $escaped . '"'; // bnode normal mode
if($o->datatype === self::RDF_LANGSTRING) { $quad .= $p->value;
if($o->language) { }
$quad .= "@{$o->language}"; $quad .= ' ';
}
} else if($o->datatype !== self::XSD_STRING) { // object is IRI, bnode, or literal
$quad .= "^^<{$o->datatype}>"; if ($o->type === 'IRI') {
} $quad .= "<{$o->value}>";
} } else if ($o->type === 'blank node') {
if ($bnode !== null) {
// graph // normalization mode
if($g !== null) { $quad .= ($o->value === $bnode) ? '_:a' : '_:z';
if(strpos($g, '_:') !== 0) { } else {
$quad .= " <$g>"; // normal mode
} else if($bnode) { $quad .= $o->value;
$quad .= ' _:g'; }
} else { } else {
$quad .= " $g"; $escaped = str_replace(
} ['\\', "\t", "\n", "\r", '"'], ['\\\\', '\t', '\n', '\r', '\"'], $o->value);
} $quad .= '"' . $escaped . '"';
if ($o->datatype === self::RDF_LANGSTRING) {
$quad .= " .\n"; if ($o->language) {
return $quad; $quad .= "@{$o->language}";
} }
} else if ($o->datatype !== self::XSD_STRING) {
/** $quad .= "^^<{$o->datatype}>";
* Registers a processor-specific RDF dataset parser by content-type. }
* Global parsers will no longer be used by this processor. }
*
* @param string $content_type the content-type for the parser. // graph
* @param callable $parser(input) the parser function (takes a string as if ($g !== null) {
* a parameter and returns an RDF dataset). if (strpos($g, '_:') !== 0) {
*/ $quad .= " <$g>";
public function registerRDFParser($content_type, $parser) { } else if ($bnode) {
if($this->rdfParsers === null) { $quad .= ' _:g';
$this->rdfParsers = new stdClass(); } else {
} $quad .= " $g";
$this->rdfParsers->{$content_type} = $parser; }
} }
/** $quad .= " .\n";
* Unregisters a process-specific RDF dataset parser by content-type. If return $quad;
* there are no remaining processor-specific parsers, then the global }
* parsers will be re-enabled.
* /**
* @param string $content_type the content-type for the parser. * Registers a processor-specific RDF dataset parser by content-type.
*/ * Global parsers will no longer be used by this processor.
public function unregisterRDFParser($content_type) { *
if($this->rdfParsers !== null && * @param string $content_type the content-type for the parser.
property_exists($this->rdfParsers, $content_type)) { * @param callable $parser(input) the parser function (takes a string as
unset($this->rdfParsers->{$content_type}); * a parameter and returns an RDF dataset).
if(count(get_object_vars($content_type)) === 0) { */
$this->rdfParsers = null; public function registerRDFParser($content_type, $parser)
} {
} if ($this->rdfParsers === null) {
} $this->rdfParsers = new stdClass();
}
/** $this->rdfParsers->{$content_type} = $parser;
* If $value is an array, returns $value, otherwise returns an array }
* containing $value as the only element.
* /**
* @param mixed $value the value. * Unregisters a process-specific RDF dataset parser by content-type. If
* * there are no remaining processor-specific parsers, then the global
* @return array an array. * parsers will be re-enabled.
*/ *
public static function arrayify($value) { * @param string $content_type the content-type for the parser.
return is_array($value) ? $value : array($value); */
} public function unregisterRDFParser($content_type)
{
/** if ($this->rdfParsers !== null &&
* Clones an object, array, or string/number. property_exists($this->rdfParsers, $content_type)) {
* unset($this->rdfParsers->{$content_type});
* @param mixed $value the value to clone. if (count(get_object_vars($content_type)) === 0) {
* $this->rdfParsers = null;
* @return mixed the cloned value. }
*/ }
public static function copy($value) { }
if(is_object($value) || is_array($value)) {
return unserialize(serialize($value)); /**
} * If $value is an array, returns $value, otherwise returns an array
return $value; * containing $value as the only element.
} *
* @param mixed $value the value.
/** *
* Sets the value of a key for the given array if that property * @return array an array.
* has not already been set. */
* public static function arrayify($value)
* @param &assoc $arr the object to update. {
* @param string $key the key to update. return is_array($value) ? $value : [$value];
* @param mixed $value the value to set. }
*/
public static function setdefault(&$arr, $key, $value) { /**
isset($arr[$key]) or $arr[$key] = $value; * Clones an object, array, or string/number.
} *
* @param mixed $value the value to clone.
/** *
* Sets default values for keys in the given array. * @return mixed the cloned value.
* */
* @param &assoc $arr the object to update. public static function copy($value)
* @param assoc $defaults the default keys and values. {
*/ if (is_object($value) || is_array($value)) {
public static function setdefaults(&$arr, $defaults) { return unserialize(serialize($value));
foreach($defaults as $key => $value) { }
self::setdefault($arr, $key, $value); return $value;
} }
}
/**
/** * Sets the value of a key for the given array if that property
* Recursively compacts an element using the given active context. All values * has not already been set.
* must be in expanded form before this method is called. *
* * @param &assoc $arr the object to update.
* @param stdClass $active_ctx the active context to use. * @param string $key the key to update.
* @param mixed $active_property the compacted property with the element * @param mixed $value the value to set.
* to compact, null for none. */
* @param mixed $element the element to compact. public static function setdefault(&$arr, $key, $value)
* @param assoc $options the compaction options. {
* isset($arr[$key]) or $arr[$key] = $value;
* @return mixed the compacted value. }
*/
protected function _compact( /**
$active_ctx, $active_property, $element, $options) { * Sets default values for keys in the given array.
// recursively compact array *
if(is_array($element)) { * @param &assoc $arr the object to update.
$rval = array(); * @param assoc $defaults the default keys and values.
foreach($element as $e) { */
// compact, dropping any null values public static function setdefaults(&$arr, $defaults)
$compacted = $this->_compact( {
$active_ctx, $active_property, $e, $options); foreach ($defaults as $key => $value) {
if($compacted !== null) { self::setdefault($arr, $key, $value);
$rval[] = $compacted; }
} }
}
if($options['compactArrays'] && count($rval) === 1) { /**
// use single element if no container is specified * Recursively compacts an element using the given active context. All values
$container = self::getContextValue( * must be in expanded form before this method is called.
$active_ctx, $active_property, '@container'); *
if($container === null) { * @param stdClass $active_ctx the active context to use.
$rval = $rval[0]; * @param mixed $active_property the compacted property with the element
} * to compact, null for none.
} * @param mixed $element the element to compact.
return $rval; * @param assoc $options the compaction options.
} *
* @return mixed the compacted value.
// recursively compact object */
if(is_object($element)) { protected function _compact(
// do value compaction on @values and subject references $active_ctx, $active_property, $element, $options)
if(self::_isValue($element) || self::_isSubjectReference($element)) { {
return $this->_compactValue($active_ctx, $active_property, $element); // recursively compact array
} if (is_array($element)) {
$rval = [];
// FIXME: avoid misuse of active property as an expanded property? foreach ($element as $e) {
$inside_reverse = ($active_property === '@reverse'); // compact, dropping any null values
$compacted = $this->_compact(
// process element keys in order $active_ctx, $active_property, $e, $options);
$keys = array_keys((array)$element); if ($compacted !== null) {
sort($keys); $rval[] = $compacted;
$rval = new stdClass(); }
foreach($keys as $expanded_property) { }
$expanded_value = $element->{$expanded_property}; if ($options['compactArrays'] && count($rval) === 1) {
// use single element if no container is specified
// compact @id and @type(s) $container = self::getContextValue(
if($expanded_property === '@id' || $expanded_property === '@type') { $active_ctx, $active_property, '@container');
if(is_string($expanded_value)) { if ($container === null) {
// compact single @id $rval = $rval[0];
$compacted_value = $this->_compactIri( }
$active_ctx, $expanded_value, null, }
array('vocab' => ($expanded_property === '@type'))); return $rval;
} else { }
// expanded value must be a @type array
$compacted_value = array(); // recursively compact object
foreach($expanded_value as $ev) { if (is_object($element)) {
$compacted_value[] = $this->_compactIri( if ($options['link'] && property_exists($element, '@id') &&
$active_ctx, $ev, null, array('vocab' => true)); isset($options['link'][$element->{'@id'}])) {
} // check for a linked element to reuse
} $linked = $options['link'][$element->{'@id'}];
foreach ($linked as $link) {
// use keyword alias and add value if ($link['expanded'] === $element) {
$alias = $this->_compactIri($active_ctx, $expanded_property); return $link['compacted'];
$is_array = (is_array($compacted_value) && }
count($expanded_value) === 0); }
self::addValue( }
$rval, $alias, $compacted_value,
array('propertyIsArray' => $is_array)); // do value compaction on @values and subject references
continue; if (self::_isValue($element) || self::_isSubjectReference($element)) {
} $rval = $this->_compactValue($active_ctx, $active_property, $element);
if ($options['link'] && self::_isSubjectReference($element)) {
// handle @reverse // store linked element
if($expanded_property === '@reverse') { if (!isset($options['link'][$element->{'@id'}])) {
// recursively compact expanded value $options['link'][$element->{'@id'}] = [];
$compacted_value = $this->_compact( }
$active_ctx, '@reverse', $expanded_value, $options); $options['link'][$element->{'@id'}][] = [
'expanded' => $element, 'compacted' => $rval];
// handle double-reversed properties }
foreach($compacted_value as $compacted_property => $value) { return $rval;
if(property_exists($active_ctx->mappings, $compacted_property) && }
$active_ctx->mappings->{$compacted_property} &&
$active_ctx->mappings->{$compacted_property}->reverse) { // FIXME: avoid misuse of active property as an expanded property?
$container = self::getContextValue( $inside_reverse = ($active_property === '@reverse');
$active_ctx, $compacted_property, '@container');
$use_array = ($container === '@set' || $rval = new stdClass();
!$options['compactArrays']);
self::addValue( if ($options['link'] && property_exists($element, '@id')) {
$rval, $compacted_property, $value, // store linked element
array('propertyIsArray' => $use_array)); if (!isset($options['link'][$element->{'@id'}])) {
unset($compacted_value->{$compacted_property}); $options['link'][$element->{'@id'}] = [];
} }
} $options['link'][$element->{'@id'}][] = [
'expanded' => $element, 'compacted' => $rval];
if(count(array_keys((array)$compacted_value)) > 0) { }
// use keyword alias and add value
$alias = $this->_compactIri($active_ctx, $expanded_property); // process element keys in order
self::addValue($rval, $alias, $compacted_value); $keys = array_keys((array) $element);
} sort($keys);
foreach ($keys as $expanded_property) {
continue; $expanded_value = $element->{$expanded_property};
}
// compact @id and @type(s)
// handle @index property if ($expanded_property === '@id' || $expanded_property === '@type') {
if($expanded_property === '@index') { if (is_string($expanded_value)) {
// drop @index if inside an @index container // compact single @id
$container = self::getContextValue( $compacted_value = $this->_compactIri(
$active_ctx, $active_property, '@container'); $active_ctx, $expanded_value, null, ['vocab' => ($expanded_property === '@type')]);
if($container === '@index') { } else {
continue; // expanded value must be a @type array
} $compacted_value = [];
foreach ($expanded_value as $ev) {
// use keyword alias and add value $compacted_value[] = $this->_compactIri(
$alias = $this->_compactIri($active_ctx, $expanded_property); $active_ctx, $ev, null, ['vocab' => true]);
self::addValue($rval, $alias, $expanded_value); }
continue; }
}
// use keyword alias and add value
// Note: expanded value must be an array due to expansion algorithm. $alias = $this->_compactIri($active_ctx, $expanded_property);
$is_array = (is_array($compacted_value) &&
// preserve empty arrays count($expanded_value) === 0);
if(count($expanded_value) === 0) { self::addValue(
$item_active_property = $this->_compactIri( $rval, $alias, $compacted_value, ['propertyIsArray' => $is_array]);
$active_ctx, $expanded_property, $expanded_value, continue;
array('vocab' => true), $inside_reverse); }
self::addValue(
$rval, $item_active_property, array(), // handle @reverse
array('propertyIsArray' => true)); if ($expanded_property === '@reverse') {
} // recursively compact expanded value
$compacted_value = $this->_compact(
// recusively process array values $active_ctx, '@reverse', $expanded_value, $options);
foreach($expanded_value as $expanded_item) {
// compact property and get container type // handle double-reversed properties
$item_active_property = $this->_compactIri( foreach ($compacted_value as $compacted_property => $value) {
$active_ctx, $expanded_property, $expanded_item, if (property_exists($active_ctx->mappings, $compacted_property) &&
array('vocab' => true), $inside_reverse); $active_ctx->mappings->{$compacted_property} &&
$container = self::getContextValue( $active_ctx->mappings->{$compacted_property}->reverse) {
$active_ctx, $item_active_property, '@container'); $container = self::getContextValue(
$active_ctx, $compacted_property, '@container');
// get @list value if appropriate $use_array = ($container === '@set' ||
$is_list = self::_isList($expanded_item); !$options['compactArrays']);
$list = null; self::addValue(
if($is_list) { $rval, $compacted_property, $value, ['propertyIsArray' => $use_array]);
$list = $expanded_item->{'@list'}; unset($compacted_value->{$compacted_property});
} }
}
// recursively compact expanded item
$compacted_item = $this->_compact( if (count(array_keys((array) $compacted_value)) > 0) {
$active_ctx, $item_active_property, // use keyword alias and add value
$is_list ? $list : $expanded_item, $options); $alias = $this->_compactIri($active_ctx, $expanded_property);
self::addValue($rval, $alias, $compacted_value);
// handle @list }
if($is_list) {
// ensure @list value is an array continue;
$compacted_item = self::arrayify($compacted_item); }
if($container !== '@list') { // handle @index property
// wrap using @list alias if ($expanded_property === '@index') {
$compacted_item = (object)array( // drop @index if inside an @index container
$this->_compactIri($active_ctx, '@list') => $compacted_item); $container = self::getContextValue(
$active_ctx, $active_property, '@container');
// include @index from expanded @list, if any if ($container === '@index') {
if(property_exists($expanded_item, '@index')) { continue;
$compacted_item->{$this->_compactIri($active_ctx, '@index')} = }
$expanded_item->{'@index'};
} // use keyword alias and add value
} else if(property_exists($rval, $item_active_property)) { $alias = $this->_compactIri($active_ctx, $expanded_property);
// can't use @list container for more than 1 list self::addValue($rval, $alias, $expanded_value);
throw new JsonLdException( continue;
'JSON-LD compact error; property has a "@list" @container ' . }
'rule but there is more than a single @list that matches ' .
'the compacted term in the document. Compaction might mix ' . // skip array processing for keywords that aren't @graph or @list
'unwanted items into the list.', 'jsonld.SyntaxError', if ($expanded_property !== '@graph' && $expanded_property !== '@list' &&
'compaction to list of lists'); self::_isKeyword($expanded_property)) {
} // use keyword alias and add value as is
} $alias = $this->_compactIri($active_ctx, $expanded_property);
self::addValue($rval, $alias, $expanded_value);
// handle language and index maps continue;
if($container === '@language' || $container === '@index') { }
// get or create the map object
if(property_exists($rval, $item_active_property)) { // Note: expanded value must be an array due to expansion algorithm.
$map_object = $rval->{$item_active_property}; // preserve empty arrays
} else { if (count($expanded_value) === 0) {
$rval->{$item_active_property} = $map_object = new stdClass(); $item_active_property = $this->_compactIri(
} $active_ctx, $expanded_property, $expanded_value, ['vocab' => true], $inside_reverse);
self::addValue(
// if container is a language map, simplify compacted value to $rval, $item_active_property, [], ['propertyIsArray' => true]);
// a simple string }
if($container === '@language' && self::_isValue($compacted_item)) {
$compacted_item = $compacted_item->{'@value'}; // recusively process array values
} foreach ($expanded_value as $expanded_item) {
// compact property and get container type
// add compact value to map object using key from expanded value $item_active_property = $this->_compactIri(
// based on the container type $active_ctx, $expanded_property, $expanded_item, ['vocab' => true], $inside_reverse);
self::addValue( $container = self::getContextValue(
$map_object, $expanded_item->{$container}, $compacted_item); $active_ctx, $item_active_property, '@container');
} else {
// use an array if: compactArrays flag is false, // get @list value if appropriate
// @container is @set or @list, value is an empty $is_list = self::_isList($expanded_item);
// array, or key is @graph $list = null;
$is_array = (!$options['compactArrays'] || if ($is_list) {
$container === '@set' || $container === '@list' || $list = $expanded_item->{'@list'};
(is_array($compacted_item) && count($compacted_item) === 0) || }
$expanded_property === '@list' ||
$expanded_property === '@graph'); // recursively compact expanded item
$compacted_item = $this->_compact(
// add compact value $active_ctx, $item_active_property, $is_list ? $list : $expanded_item, $options);
self::addValue(
$rval, $item_active_property, $compacted_item, // handle @list
array('propertyIsArray' => $is_array)); if ($is_list) {
} // ensure @list value is an array
} $compacted_item = self::arrayify($compacted_item);
}
if ($container !== '@list') {
return $rval; // wrap using @list alias
} $compacted_item = (object) [
$this->_compactIri($active_ctx, '@list') => $compacted_item];
// only primitives remain which are already compact
return $element; // include @index from expanded @list, if any
} if (property_exists($expanded_item, '@index')) {
$compacted_item->{$this->_compactIri($active_ctx, '@index')} = $expanded_item->{'@index'};
/** }
* Recursively expands an element using the given context. Any context in } else if (property_exists($rval, $item_active_property)) {
* the element will be removed. All context URLs must have been retrieved // can't use @list container for more than 1 list
* before calling this method. throw new JsonLdException(
* 'JSON-LD compact error; property has a "@list" @container ' .
* @param stdClass $active_ctx the active context to use. 'rule but there is more than a single @list that matches ' .
* @param mixed $active_property the property for the element, null for none. 'the compacted term in the document. Compaction might mix ' .
* @param mixed $element the element to expand. 'unwanted items into the list.', 'jsonld.SyntaxError', 'compaction to list of lists');
* @param assoc $options the expansion options. }
* @param bool $inside_list true if the property is a list, false if not. }
*
* @return mixed the expanded value. // handle language and index maps
*/ if ($container === '@language' || $container === '@index') {
protected function _expand( // get or create the map object
$active_ctx, $active_property, $element, $options, $inside_list) { if (property_exists($rval, $item_active_property)) {
// nothing to expand $map_object = $rval->{$item_active_property};
if($element === null) { } else {
return $element; $rval->{$item_active_property} = $map_object = new stdClass();
} }
// recursively expand array // if container is a language map, simplify compacted value to
if(is_array($element)) { // a simple string
$rval = array(); if ($container === '@language' && self::_isValue($compacted_item)) {
$container = self::getContextValue( $compacted_item = $compacted_item->{'@value'};
$active_ctx, $active_property, '@container'); }
$inside_list = $inside_list || $container === '@list';
foreach($element as $e) { // add compact value to map object using key from expanded value
// expand element // based on the container type
$e = $this->_expand( self::addValue(
$active_ctx, $active_property, $e, $options, $inside_list); $map_object, $expanded_item->{$container}, $compacted_item);
if($inside_list && (is_array($e) || self::_isList($e))) { } else {
// lists of lists are illegal // use an array if: compactArrays flag is false,
throw new JsonLdException( // @container is @set or @list, value is an empty
'Invalid JSON-LD syntax; lists of lists are not permitted.', // array, or key is @graph
'jsonld.SyntaxError', 'list of lists'); $is_array = (!$options['compactArrays'] ||
} $container === '@set' || $container === '@list' ||
// drop null values (is_array($compacted_item) && count($compacted_item) === 0) ||
if($e !== null) { $expanded_property === '@list' ||
if(is_array($e)) { $expanded_property === '@graph');
$rval = array_merge($rval, $e);
} else { // add compact value
$rval[] = $e; self::addValue(
} $rval, $item_active_property, $compacted_item, ['propertyIsArray' => $is_array]);
} }
} }
return $rval; }
}
return $rval;
if(!is_object($element)) { }
// drop free-floating scalars that are not in lists
if(!$inside_list && // only primitives remain which are already compact
($active_property === null || return $element;
$this->_expandIri($active_ctx, $active_property, }
array('vocab' => true)) === '@graph')) {
return null; /**
} * Recursively expands an element using the given context. Any context in
* the element will be removed. All context URLs must have been retrieved
// expand element according to value expansion rules * before calling this method.
return $this->_expandValue($active_ctx, $active_property, $element); *
} * @param stdClass $active_ctx the active context to use.
* @param mixed $active_property the property for the element, null for none.
// recursively expand object: * @param mixed $element the element to expand.
* @param assoc $options the expansion options.
// if element has a context, process it * @param bool $inside_list true if the property is a list, false if not.
if(property_exists($element, '@context')) { *
$active_ctx = $this->_processContext( * @return mixed the expanded value.
$active_ctx, $element->{'@context'}, $options); */
} protected function _expand(
$active_ctx, $active_property, $element, $options, $inside_list)
// expand the active property {
$expanded_active_property = $this->_expandIri( // nothing to expand
$active_ctx, $active_property, array('vocab' => true)); if ($element === null) {
return $element;
$rval = new stdClass(); }
$keys = array_keys((array)$element);
sort($keys); // recursively expand array
foreach($keys as $key) { if (is_array($element)) {
$value = $element->{$key}; $rval = [];
$container = self::getContextValue(
if($key === '@context') { $active_ctx, $active_property, '@container');
continue; $inside_list = $inside_list || $container === '@list';
} foreach ($element as $e) {
// expand element
// expand key to IRI $e = $this->_expand(
$expanded_property = $this->_expandIri( $active_ctx, $active_property, $e, $options, $inside_list);
$active_ctx, $key, array('vocab' => true)); if ($inside_list && (is_array($e) || self::_isList($e))) {
// lists of lists are illegal
// drop non-absolute IRI keys that aren't keywords throw new JsonLdException(
if($expanded_property === null || 'Invalid JSON-LD syntax; lists of lists are not permitted.', 'jsonld.SyntaxError', 'list of lists');
!(self::_isAbsoluteIri($expanded_property) || }
self::_isKeyword($expanded_property))) { // drop null values
continue; if ($e !== null) {
} if (is_array($e)) {
$rval = array_merge($rval, $e);
if(self::_isKeyword($expanded_property)) { } else {
if($expanded_active_property === '@reverse') { $rval[] = $e;
throw new JsonLdException( }
'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' . }
'property.', 'jsonld.SyntaxError', 'invalid reverse property map', }
array('value' => $value)); return $rval;
} }
if(property_exists($rval, $expanded_property)) {
throw new JsonLdException( if (!is_object($element)) {
'Invalid JSON-LD syntax; colliding keywords detected.', // drop free-floating scalars that are not in lists
'jsonld.SyntaxError', 'colliding keywords', if (!$inside_list &&
array('keyword' => $expanded_property)); ($active_property === null ||
} $this->_expandIri($active_ctx, $active_property, ['vocab' => true]) === '@graph')) {
} return null;
}
// syntax error if @id is not a string
if($expanded_property === '@id' && !is_string($value)) { // expand element according to value expansion rules
if(!isset($options['isFrame']) || !$options['isFrame']) { return $this->_expandValue($active_ctx, $active_property, $element);
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@id" value must a string.',
'jsonld.SyntaxError', 'invalid @id value', // recursively expand object:
array('value' => $value)); // if element has a context, process it
} if (property_exists($element, '@context')) {
if(!is_object($value)) { $active_ctx = $this->_processContext(
throw new JsonLdException( $active_ctx, $element->{'@context'}, $options);
'Invalid JSON-LD syntax; "@id" value must a string or an object.', }
'jsonld.SyntaxError', 'invalid @id value',
array('value' => $value)); // expand the active property
} $expanded_active_property = $this->_expandIri(
} $active_ctx, $active_property, ['vocab' => true]);
// validate @type value $rval = new stdClass();
if($expanded_property === '@type') { $keys = array_keys((array) $element);
$this->_validateTypeValue($value); sort($keys);
} foreach ($keys as $key) {
$value = $element->{$key};
// @graph must be an array or an object
if($expanded_property === '@graph' && if ($key === '@context') {
!(is_object($value) || is_array($value))) { continue;
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@graph" value must not be an ' .
'object or an array.', 'jsonld.SyntaxError', // expand key to IRI
'invalid @graph value', array('value' => $value)); $expanded_property = $this->_expandIri(
} $active_ctx, $key, ['vocab' => true]);
// @value must not be an object or an array // drop non-absolute IRI keys that aren't keywords
if($expanded_property === '@value' && if ($expanded_property === null ||
(is_object($value) || is_array($value))) { !(self::_isAbsoluteIri($expanded_property) ||
throw new JsonLdException( self::_isKeyword($expanded_property))) {
'Invalid JSON-LD syntax; "@value" value must not be an ' . continue;
'object or an array.', 'jsonld.SyntaxError', }
'invalid value object value', array('value' => $value));
} if (self::_isKeyword($expanded_property)) {
if ($expanded_active_property === '@reverse') {
// @language must be a string throw new JsonLdException(
if($expanded_property === '@language') { 'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' .
if(!is_string($value)) { 'property.', 'jsonld.SyntaxError', 'invalid reverse property map', ['value' => $value]);
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@language" value must not be a string.', if (property_exists($rval, $expanded_property)) {
'jsonld.SyntaxError', 'invalid language-tagged string', throw new JsonLdException(
array('value' => $value)); 'Invalid JSON-LD syntax; colliding keywords detected.', 'jsonld.SyntaxError', 'colliding keywords', ['keyword' => $expanded_property]);
} }
// ensure language value is lowercase }
$value = strtolower($value);
} // syntax error if @id is not a string
if ($expanded_property === '@id' && !is_string($value)) {
// @index must be a string if (!isset($options['isFrame']) || !$options['isFrame']) {
if($expanded_property === '@index') { throw new JsonLdException(
if(!is_string($value)) { 'Invalid JSON-LD syntax; "@id" value must a string.', 'jsonld.SyntaxError', 'invalid @id value', ['value' => $value]);
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@index" value must be a string.', if (!is_object($value)) {
'jsonld.SyntaxError', 'invalid @index value', throw new JsonLdException(
array('value' => $value)); 'Invalid JSON-LD syntax; "@id" value must a string or an object.', 'jsonld.SyntaxError', 'invalid @id value', ['value' => $value]);
} }
} }
// @reverse must be an object // validate @type value
if($expanded_property === '@reverse') { if ($expanded_property === '@type') {
if(!is_object($value)) { $this->_validateTypeValue($value);
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@reverse" value must be an object.',
'jsonld.SyntaxError', 'invalid @reverse value', // @graph must be an array or an object
array('value' => $value)); if ($expanded_property === '@graph' &&
} !(is_object($value) || is_array($value))) {
throw new JsonLdException(
$expanded_value = $this->_expand( 'Invalid JSON-LD syntax; "@graph" value must not be an ' .
$active_ctx, '@reverse', $value, $options, $inside_list); 'object or an array.', 'jsonld.SyntaxError', 'invalid @graph value', ['value' => $value]);
}
// properties double-reversed
if(property_exists($expanded_value, '@reverse')) { // @value must not be an object or an array
foreach($expanded_value->{'@reverse'} as $rproperty => $rvalue) { if ($expanded_property === '@value' &&
self::addValue( (is_object($value) || is_array($value))) {
$rval, $rproperty, $rvalue, array('propertyIsArray' => true)); throw new JsonLdException(
} 'Invalid JSON-LD syntax; "@value" value must not be an ' .
} 'object or an array.', 'jsonld.SyntaxError', 'invalid value object value', ['value' => $value]);
}
// FIXME: can this be merged with code below to simplify?
// merge in all reversed properties // @language must be a string
if(property_exists($rval, '@reverse')) { if ($expanded_property === '@language') {
$reverse_map = $rval->{'@reverse'}; if ($value === null) {
} else { // drop null @language values, they expand as if they didn't exist
$reverse_map = null; continue;
} }
foreach($expanded_value as $property => $items) { if (!is_string($value)) {
if($property === '@reverse') { throw new JsonLdException(
continue; 'Invalid JSON-LD syntax; "@language" value must not be a string.', 'jsonld.SyntaxError', 'invalid language-tagged string', ['value' => $value]);
} }
if($reverse_map === null) { // ensure language value is lowercase
$reverse_map = $rval->{'@reverse'} = new stdClass(); $value = strtolower($value);
} }
self::addValue(
$reverse_map, $property, array(), // @index must be a string
array('propertyIsArray' => true)); if ($expanded_property === '@index') {
foreach($items as $item) { if (!is_string($value)) {
if(self::_isValue($item) || self::_isList($item)) { throw new JsonLdException(
throw new JsonLdException( 'Invalid JSON-LD syntax; "@index" value must be a string.', 'jsonld.SyntaxError', 'invalid @index value', ['value' => $value]);
'Invalid JSON-LD syntax; "@reverse" value must not be a ' + }
'@value or an @list.', 'jsonld.SyntaxError', }
'invalid reverse property value',
array('value' => $expanded_value)); // @reverse must be an object
} if ($expanded_property === '@reverse') {
self::addValue( if (!is_object($value)) {
$reverse_map, $property, $item, throw new JsonLdException(
array('propertyIsArray' => true)); 'Invalid JSON-LD syntax; "@reverse" value must be an object.', 'jsonld.SyntaxError', 'invalid @reverse value', ['value' => $value]);
} }
}
$expanded_value = $this->_expand(
continue; $active_ctx, '@reverse', $value, $options, $inside_list);
}
// properties double-reversed
$container = self::getContextValue($active_ctx, $key, '@container'); if (property_exists($expanded_value, '@reverse')) {
foreach ($expanded_value->{'@reverse'} as $rproperty => $rvalue) {
if($container === '@language' && is_object($value)) { self::addValue(
// handle language map container (skip if value is not an object) $rval, $rproperty, $rvalue, ['propertyIsArray' => true]);
$expanded_value = $this->_expandLanguageMap($value); }
} else if($container === '@index' && is_object($value)) { }
// handle index container (skip if value is not an object)
$expanded_value = array(); // FIXME: can this be merged with code below to simplify?
$value_keys = array_keys((array)$value); // merge in all reversed properties
sort($value_keys); if (property_exists($rval, '@reverse')) {
foreach($value_keys as $value_key) { $reverse_map = $rval->{'@reverse'};
$val = $value->{$value_key}; } else {
$val = self::arrayify($val); $reverse_map = null;
$val = $this->_expand($active_ctx, $key, $val, $options, false); }
foreach($val as $item) { foreach ($expanded_value as $property => $items) {
if(!property_exists($item, '@index')) { if ($property === '@reverse') {
$item->{'@index'} = $value_key; continue;
} }
$expanded_value[] = $item; if ($reverse_map === null) {
} $reverse_map = $rval->{'@reverse'} = new stdClass();
} }
} else { self::addValue(
// recurse into @list or @set $reverse_map, $property, [], ['propertyIsArray' => true]);
$is_list = ($expanded_property === '@list'); foreach ($items as $item) {
if($is_list || $expanded_property === '@set') { if (self::_isValue($item) || self::_isList($item)) {
$next_active_property = $active_property; throw new JsonLdException(
if($is_list && $expanded_active_property === '@graph') { 'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
$next_active_property = null; '@value or an @list.', 'jsonld.SyntaxError', 'invalid reverse property value', ['value' => $expanded_value]);
} }
$expanded_value = $this->_expand( self::addValue(
$active_ctx, $next_active_property, $value, $options, $is_list); $reverse_map, $property, $item, ['propertyIsArray' => true]);
if($is_list && self::_isList($expanded_value)) { }
throw new JsonLdException( }
'Invalid JSON-LD syntax; lists of lists are not permitted.',
'jsonld.SyntaxError', 'list of lists'); continue;
} }
} else {
// recursively expand value with key as new active property $container = self::getContextValue($active_ctx, $key, '@container');
$expanded_value = $this->_expand(
$active_ctx, $key, $value, $options, false); if ($container === '@language' && is_object($value)) {
} // handle language map container (skip if value is not an object)
} $expanded_value = $this->_expandLanguageMap($value);
} else if ($container === '@index' && is_object($value)) {
// drop null values if property is not @value // handle index container (skip if value is not an object)
if($expanded_value === null && $expanded_property !== '@value') { $expanded_value = [];
continue; $value_keys = array_keys((array) $value);
} sort($value_keys);
foreach ($value_keys as $value_key) {
// convert expanded value to @list if container specifies it $val = $value->{$value_key};
if($expanded_property !== '@list' && !self::_isList($expanded_value) && $val = self::arrayify($val);
$container === '@list') { $val = $this->_expand($active_ctx, $key, $val, $options, false);
// ensure expanded value is an array foreach ($val as $item) {
$expanded_value = (object)array( if (!property_exists($item, '@index')) {
'@list' => self::arrayify($expanded_value)); $item->{'@index'} = $value_key;
} }
$expanded_value[] = $item;
// FIXME: can this be merged with code above to simplify? }
// merge in reverse properties }
if(property_exists($active_ctx->mappings, $key) && } else {
$active_ctx->mappings->{$key} && // recurse into @list or @set
$active_ctx->mappings->{$key}->reverse) { $is_list = ($expanded_property === '@list');
if(property_exists($rval, '@reverse')) { if ($is_list || $expanded_property === '@set') {
$reverse_map = $rval->{'@reverse'}; $next_active_property = $active_property;
} else { if ($is_list && $expanded_active_property === '@graph') {
$reverse_map = $rval->{'@reverse'} = new stdClass(); $next_active_property = null;
} }
$expanded_value = self::arrayify($expanded_value); $expanded_value = $this->_expand(
foreach($expanded_value as $item) { $active_ctx, $next_active_property, $value, $options, $is_list);
if(self::_isValue($item) || self::_isList($item)) { if ($is_list && self::_isList($expanded_value)) {
throw new JsonLdException( throw new JsonLdException(
'Invalid JSON-LD syntax; "@reverse" value must not be a ' + 'Invalid JSON-LD syntax; lists of lists are not permitted.', 'jsonld.SyntaxError', 'list of lists');
'@value or an @list.', 'jsonld.SyntaxError', }
'invalid reverse property value', } else {
array('value' => $expanded_value)); // recursively expand value with key as new active property
} $expanded_value = $this->_expand(
self::addValue( $active_ctx, $key, $value, $options, false);
$reverse_map, $expanded_property, $item, }
array('propertyIsArray' => true)); }
}
continue; // drop null values if property is not @value
} if ($expanded_value === null && $expanded_property !== '@value') {
continue;
// add value for property }
// use an array except for certain keywords
$use_array = (!in_array( // convert expanded value to @list if container specifies it
$expanded_property, array( if ($expanded_property !== '@list' && !self::_isList($expanded_value) &&
'@index', '@id', '@type', '@value', '@language'))); $container === '@list') {
self::addValue( // ensure expanded value is an array
$rval, $expanded_property, $expanded_value, $expanded_value = (object) [
array('propertyIsArray' => $use_array)); '@list' => self::arrayify($expanded_value)];
} }
// get property count on expanded output // FIXME: can this be merged with code above to simplify?
$keys = array_keys((array)$rval); // merge in reverse properties
$count = count($keys); if (property_exists($active_ctx->mappings, $key) &&
$active_ctx->mappings->{$key} &&
// @value must only have @language or @type $active_ctx->mappings->{$key}->reverse) {
if(property_exists($rval, '@value')) { if (property_exists($rval, '@reverse')) {
// @value must only have @language or @type $reverse_map = $rval->{'@reverse'};
if(property_exists($rval, '@type') && } else {
property_exists($rval, '@language')) { $reverse_map = $rval->{'@reverse'} = new stdClass();
throw new JsonLdException( }
'Invalid JSON-LD syntax; an element containing "@value" may not ' . $expanded_value = self::arrayify($expanded_value);
'contain both "@type" and "@language".', foreach ($expanded_value as $item) {
'jsonld.SyntaxError', 'invalid value object', if (self::_isValue($item) || self::_isList($item)) {
array('element' => $rval)); throw new JsonLdException(
} 'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
$valid_count = $count - 1; '@value or an @list.', 'jsonld.SyntaxError', 'invalid reverse property value', ['value' => $expanded_value]);
if(property_exists($rval, '@type')) { }
$valid_count -= 1; self::addValue(
} $reverse_map, $expanded_property, $item, ['propertyIsArray' => true]);
if(property_exists($rval, '@index')) { }
$valid_count -= 1; continue;
} }
if(property_exists($rval, '@language')) {
$valid_count -= 1; // add value for property
} // use an array except for certain keywords
if($valid_count !== 0) { $use_array = (!in_array(
throw new JsonLdException( $expanded_property, [
'Invalid JSON-LD syntax; an element containing "@value" may only ' . '@index', '@id', '@type', '@value', '@language']));
'have an "@index" property and at most one other property ' . self::addValue(
'which can be "@type" or "@language".', $rval, $expanded_property, $expanded_value, ['propertyIsArray' => $use_array]);
'jsonld.SyntaxError', 'invalid value object', }
array('element' => $rval));
} // get property count on expanded output
// drop null @values $keys = array_keys((array) $rval);
if($rval->{'@value'} === null) { $count = count($keys);
$rval = null;
} else if(property_exists($rval, '@language') && // @value must only have @language or @type
!is_string($rval->{'@value'})) { if (property_exists($rval, '@value')) {
// if @language is present, @value must be a string // @value must only have @language or @type
throw new JsonLdException( if (property_exists($rval, '@type') &&
'Invalid JSON-LD syntax; only strings may be language-tagged.', property_exists($rval, '@language')) {
'jsonld.SyntaxError', 'invalid language-tagged value', throw new JsonLdException(
array('element' => $rval)); 'Invalid JSON-LD syntax; an element containing "@value" may not ' .
} else if(property_exists($rval, '@type') && 'contain both "@type" and "@language".', 'jsonld.SyntaxError', 'invalid value object', ['element' => $rval]);
(!self::_isAbsoluteIri($rval->{'@type'}) || }
strpos($rval->{'@type'}, '_:') === 0)) { $valid_count = $count - 1;
throw new JsonLdException( if (property_exists($rval, '@type')) {
'Invalid JSON-LD syntax; an element containing "@value" ' . $valid_count -= 1;
'and "@type" must have an absolute IRI for the value ' . }
'of "@type".', 'jsonld.SyntaxError', 'invalid typed value', if (property_exists($rval, '@index')) {
array('element' => $rval)); $valid_count -= 1;
} }
} else if(property_exists($rval, '@type') && !is_array($rval->{'@type'})) { if (property_exists($rval, '@language')) {
// convert @type to an array $valid_count -= 1;
$rval->{'@type'} = array($rval->{'@type'}); }
} else if(property_exists($rval, '@set') || if ($valid_count !== 0) {
property_exists($rval, '@list')) { throw new JsonLdException(
// handle @set and @list 'Invalid JSON-LD syntax; an element containing "@value" may only ' .
if($count > 1 && !($count === 2 && property_exists($rval, '@index'))) { 'have an "@index" property and at most one other property ' .
throw new JsonLdException( 'which can be "@type" or "@language".', 'jsonld.SyntaxError', 'invalid value object', ['element' => $rval]);
'Invalid JSON-LD syntax; if an element has the property "@set" ' . }
'or "@list", then it can have at most one other property that is ' . // drop null @values
'"@index".', 'jsonld.SyntaxError', 'invalid set or list object', if ($rval->{'@value'} === null) {
array('element' => $rval)); $rval = null;
} } else if (property_exists($rval, '@language') &&
// optimize away @set !is_string($rval->{'@value'})) {
if(property_exists($rval, '@set')) { // if @language is present, @value must be a string
$rval = $rval->{'@set'}; throw new JsonLdException(
$keys = array_keys((array)$rval); 'Invalid JSON-LD syntax; only strings may be language-tagged.', 'jsonld.SyntaxError', 'invalid language-tagged value', ['element' => $rval]);
$count = count($keys); } else if (property_exists($rval, '@type') &&
} (!self::_isAbsoluteIri($rval->{'@type'}) ||
} else if($count === 1 && property_exists($rval, '@language')) { strpos($rval->{'@type'}, '_:') === 0)) {
// drop objects with only @language throw new JsonLdException(
$rval = null; 'Invalid JSON-LD syntax; an element containing "@value" ' .
} 'and "@type" must have an absolute IRI for the value ' .
'of "@type".', 'jsonld.SyntaxError', 'invalid typed value', ['element' => $rval]);
// drop certain top-level objects that do not occur in lists }
if(is_object($rval) && } else if (property_exists($rval, '@type') && !is_array($rval->{'@type'})) {
!$options['keepFreeFloatingNodes'] && !$inside_list && // convert @type to an array
($active_property === null || $expanded_active_property === '@graph')) { $rval->{'@type'} = [$rval->{'@type'}];
// drop empty object or top-level @value/@list, or object with only @id } else if (property_exists($rval, '@set') ||
if($count === 0 || property_exists($rval, '@value') || property_exists($rval, '@list')) {
property_exists($rval, '@list') || // handle @set and @list
($count === 1 && property_exists($rval, '@id'))) { if ($count > 1 && !($count === 2 && property_exists($rval, '@index'))) {
$rval = null; throw new JsonLdException(
} 'Invalid JSON-LD syntax; if an element has the property "@set" ' .
} 'or "@list", then it can have at most one other property that is ' .
'"@index".', 'jsonld.SyntaxError', 'invalid set or list object', ['element' => $rval]);
return $rval; }
} // optimize away @set
if (property_exists($rval, '@set')) {
/** $rval = $rval->{'@set'};
* Performs JSON-LD flattening. $keys = array_keys((array) $rval);
* $count = count($keys);
* @param array $input the expanded JSON-LD to flatten. }
* } else if ($count === 1 && property_exists($rval, '@language')) {
* @return array the flattened output. // drop objects with only @language
*/ $rval = null;
protected function _flatten($input) { }
// produce a map of all subjects and name each bnode
$namer = new UniqueNamer('_:b'); // drop certain top-level objects that do not occur in lists
$graphs = (object)array('@default' => new stdClass()); if (is_object($rval) &&
$this->_createNodeMap($input, $graphs, '@default', $namer); !$options['keepFreeFloatingNodes'] && !$inside_list &&
($active_property === null || $expanded_active_property === '@graph')) {
// add all non-default graphs to default graph // drop empty object or top-level @value/@list, or object with only @id
$default_graph = $graphs->{'@default'}; if ($count === 0 || property_exists($rval, '@value') ||
$graph_names = array_keys((array)$graphs); property_exists($rval, '@list') ||
foreach($graph_names as $graph_name) { ($count === 1 && property_exists($rval, '@id'))) {
if($graph_name === '@default') { $rval = null;
continue; }
} }
$node_map = $graphs->{$graph_name};
if(!property_exists($default_graph, $graph_name)) { return $rval;
$default_graph->{$graph_name} = (object)array( }
'@id' => $graph_name, '@graph' => array());
} /**
$subject = $default_graph->{$graph_name}; * Performs JSON-LD flattening.
if(!property_exists($subject, '@graph')) { *
$subject->{'@graph'} = array(); * @param array $input the expanded JSON-LD to flatten.
} *
$ids = array_keys((array)$node_map); * @return array the flattened output.
sort($ids); */
foreach($ids as $id) { protected function _flatten($input)
$node = $node_map->{$id}; {
// only add full subjects // produce a map of all subjects and name each bnode
if(!self::_isSubjectReference($node)) { $namer = new UniqueNamer('_:b');
$subject->{'@graph'}[] = $node; $graphs = (object) ['@default' => new stdClass()];
} $this->_createNodeMap($input, $graphs, '@default', $namer);
}
} // add all non-default graphs to default graph
$default_graph = $graphs->{'@default'};
// produce flattened output $graph_names = array_keys((array) $graphs);
$flattened = array(); foreach ($graph_names as $graph_name) {
$keys = array_keys((array)$default_graph); if ($graph_name === '@default') {
sort($keys); continue;
foreach($keys as $key) { }
$node = $default_graph->{$key}; $node_map = $graphs->{$graph_name};
// only add full subjects to top-level if (!property_exists($default_graph, $graph_name)) {
if(!self::_isSubjectReference($node)) { $default_graph->{$graph_name} = (object) [
$flattened[] = $node; '@id' => $graph_name, '@graph' => []];
} }
} $subject = $default_graph->{$graph_name};
return $flattened; if (!property_exists($subject, '@graph')) {
} $subject->{'@graph'} = [];
}
/** $ids = array_keys((array) $node_map);
* Performs JSON-LD framing. sort($ids);
* foreach ($ids as $id) {
* @param array $input the expanded JSON-LD to frame. $node = $node_map->{$id};
* @param array $frame the expanded JSON-LD frame to use. // only add full subjects
* @param assoc $options the framing options. if (!self::_isSubjectReference($node)) {
* $subject->{'@graph'}[] = $node;
* @return array the framed output. }
*/ }
protected function _frame($input, $frame, $options) { }
// create framing state
$state = (object)array( // produce flattened output
'options' => $options, $flattened = [];
'graphs' => (object)array( $keys = array_keys((array) $default_graph);
'@default' => new stdClass(), sort($keys);
'@merged' => new stdClass())); foreach ($keys as $key) {
$node = $default_graph->{$key};
// produce a map of all graphs and name each bnode // only add full subjects to top-level
// FIXME: currently uses subjects from @merged graph only if (!self::_isSubjectReference($node)) {
$namer = new UniqueNamer('_:b'); $flattened[] = $node;
$this->_createNodeMap($input, $state->graphs, '@merged', $namer); }
$state->subjects = $state->graphs->{'@merged'}; }
return $flattened;
// frame the subjects }
$framed = new ArrayObject();
$keys = array_keys((array)$state->subjects); /**
sort($keys); * Performs JSON-LD framing.
$this->_matchFrame($state, $keys, $frame, $framed, null); *
return (array)$framed; * @param array $input the expanded JSON-LD to frame.
} * @param array $frame the expanded JSON-LD frame to use.
* @param assoc $options the framing options.
/** *
* Performs normalization on the given RDF dataset. * @return array the framed output.
* */
* @param stdClass $dataset the RDF dataset to normalize. protected function _frame($input, $frame, $options)
* @param assoc $options the normalization options. {
* // create framing state
* @return mixed the normalized output. $state = (object) [
*/ 'options' => $options,
protected function _normalize($dataset, $options) { 'graphs' => (object) [
// create quads and map bnodes to their associated quads '@default' => new stdClass(),
$quads = array(); '@merged' => new stdClass()],
$bnodes = new stdClass(); 'subjectStack' => [],
foreach($dataset as $graph_name => $triples) { 'link' => new stdClass()];
if($graph_name === '@default') {
$graph_name = null; // produce a map of all graphs and name each bnode
} // FIXME: currently uses subjects from @merged graph only
foreach($triples as $triple) { $namer = new UniqueNamer('_:b');
$quad = $triple; $this->_createNodeMap($input, $state->graphs, '@merged', $namer);
if($graph_name !== null) { $state->subjects = $state->graphs->{'@merged'};
if(strpos($graph_name, '_:') === 0) {
$quad->name = (object)array( // frame the subjects
'type' => 'blank node', 'value' => $graph_name); $framed = new ArrayObject();
} else { $keys = array_keys((array) $state->subjects);
$quad->name = (object)array( sort($keys);
'type' => 'IRI', 'value' => $graph_name); $this->_matchFrame($state, $keys, $frame, $framed, null);
} return (array) $framed;
} }
$quads[] = $quad;
/**
foreach(array('subject', 'object', 'name') as $attr) { * Performs normalization on the given RDF dataset.
if(property_exists($quad, $attr) && *
$quad->{$attr}->type === 'blank node') { * @param stdClass $dataset the RDF dataset to normalize.
$id = $quad->{$attr}->value; * @param assoc $options the normalization options.
if(property_exists($bnodes, $id)) { *
$bnodes->{$id}->quads[] = $quad; * @return mixed the normalized output.
} else { */
$bnodes->{$id} = (object)array('quads' => array($quad)); protected function _normalize($dataset, $options)
} {
} // create quads and map bnodes to their associated quads
} $quads = [];
} $bnodes = new stdClass();
} foreach ($dataset as $graph_name => $triples) {
if ($graph_name === '@default') {
// mapping complete, start canonical naming $graph_name = null;
$namer = new UniqueNamer('_:c14n'); }
foreach ($triples as $triple) {
// continue to hash bnode quads while bnodes are assigned names $quad = $triple;
$unnamed = null; if ($graph_name !== null) {
$nextUnnamed = array_keys((array)$bnodes); if (strpos($graph_name, '_:') === 0) {
$duplicates = null; $quad->name = (object) [
do { 'type' => 'blank node', 'value' => $graph_name];
$unnamed = $nextUnnamed; } else {
$nextUnnamed = array(); $quad->name = (object) [
$duplicates = new stdClass(); 'type' => 'IRI', 'value' => $graph_name];
$unique = new stdClass(); }
foreach($unnamed as $bnode) { }
// hash quads for each unnamed bnode $quads[] = $quad;
$hash = $this->_hashQuads($bnode, $bnodes, $namer);
foreach (['subject', 'object', 'name'] as $attr) {
// store hash as unique or a duplicate if (property_exists($quad, $attr) &&
if(property_exists($duplicates, $hash)) { $quad->{$attr}->type === 'blank node') {
$duplicates->{$hash}[] = $bnode; $id = $quad->{$attr}->value;
$nextUnnamed[] = $bnode; if (property_exists($bnodes, $id)) {
} else if(property_exists($unique, $hash)) { $bnodes->{$id}->quads[] = $quad;
$duplicates->{$hash} = array($unique->{$hash}, $bnode); } else {
$nextUnnamed[] = $unique->{$hash}; $bnodes->{$id} = (object) ['quads' => [$quad]];
$nextUnnamed[] = $bnode; }
unset($unique->{$hash}); }
} else { }
$unique->{$hash} = $bnode; }
} }
}
// mapping complete, start canonical naming
// name unique bnodes in sorted hash order $namer = new UniqueNamer('_:c14n');
$hashes = array_keys((array)$unique);
sort($hashes); // continue to hash bnode quads while bnodes are assigned names
foreach($hashes as $hash) { $unnamed = null;
$namer->getName($unique->{$hash}); $nextUnnamed = array_keys((array) $bnodes);
} $duplicates = null;
} do {
while(count($unnamed) > count($nextUnnamed)); $unnamed = $nextUnnamed;
$nextUnnamed = [];
// enumerate duplicate hash groups in sorted order $duplicates = new stdClass();
$hashes = array_keys((array)$duplicates); $unique = new stdClass();
sort($hashes); foreach ($unnamed as $bnode) {
foreach($hashes as $hash) { // hash quads for each unnamed bnode
// process group $hash = $this->_hashQuads($bnode, $bnodes, $namer);
$group = $duplicates->{$hash};
$results = array(); // store hash as unique or a duplicate
foreach($group as $bnode) { if (property_exists($duplicates, $hash)) {
// skip already-named bnodes $duplicates->{$hash}[] = $bnode;
if($namer->isNamed($bnode)) { $nextUnnamed[] = $bnode;
continue; } else if (property_exists($unique, $hash)) {
} $duplicates->{$hash} = [$unique->{$hash}, $bnode];
$nextUnnamed[] = $unique->{$hash};
// hash bnode paths $nextUnnamed[] = $bnode;
$path_namer = new UniqueNamer('_:b'); unset($unique->{$hash});
$path_namer->getName($bnode); } else {
$results[] = $this->_hashPaths($bnode, $bnodes, $namer, $path_namer); $unique->{$hash} = $bnode;
} }
}
// name bnodes in hash order
usort($results, function($a, $b) { // name unique bnodes in sorted hash order
$a = $a->hash; $hashes = array_keys((array) $unique);
$b = $b->hash; sort($hashes);
return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); foreach ($hashes as $hash) {
}); $namer->getName($unique->{$hash});
foreach($results as $result) { }
// name all bnodes in path namer in key-entry order } while (count($unnamed) > count($nextUnnamed));
foreach($result->pathNamer->order as $bnode) {
$namer->getName($bnode); // enumerate duplicate hash groups in sorted order
} $hashes = array_keys((array) $duplicates);
} sort($hashes);
} foreach ($hashes as $hash) {
// process group
// create normalized array $group = $duplicates->{$hash};
$normalized = array(); $results = [];
foreach ($group as $bnode) {
/* Note: At this point all bnodes in the set of RDF quads have been // skip already-named bnodes
assigned canonical names, which have been stored in the 'namer' object. if ($namer->isNamed($bnode)) {
Here each quad is updated by assigning each of its bnodes its new name continue;
via the 'namer' object. */ }
// update bnode names in each quad and serialize // hash bnode paths
foreach($quads as $quad) { $path_namer = new UniqueNamer('_:b');
foreach(array('subject', 'object', 'name') as $attr) { $path_namer->getName($bnode);
if(property_exists($quad, $attr) && $results[] = $this->_hashPaths($bnode, $bnodes, $namer, $path_namer);
$quad->{$attr}->type === 'blank node' && }
strpos($quad->{$attr}->value, '_:c14n') !== 0) {
$quad->{$attr}->value = $namer->getName($quad->{$attr}->value); // name bnodes in hash order
} usort($results, function($a, $b) {
} $a = $a->hash;
$normalized[] = $this->toNQuad($quad, property_exists($quad, 'name') ? $b = $b->hash;
$quad->name->value : null); return ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
} });
foreach ($results as $result) {
// sort normalized output // name all bnodes in path namer in key-entry order
sort($normalized); foreach ($result->pathNamer->order as $bnode) {
$namer->getName($bnode);
// handle output format }
if(isset($options['format']) && $options['format']) { }
if($options['format'] === 'application/nquads') { }
return implode($normalized);
} // create normalized array
throw new JsonLdException( $normalized = [];
'Unknown output format.',
'jsonld.UnknownFormat', null, array('format' => $options['format'])); /* Note: At this point all bnodes in the set of RDF quads have been
} assigned canonical names, which have been stored in the 'namer' object.
Here each quad is updated by assigning each of its bnodes its new name
// return RDF dataset via the 'namer' object. */
return $this->parseNQuads(implode($normalized));
} // update bnode names in each quad and serialize
foreach ($quads as $quad) {
/** foreach (['subject', 'object', 'name'] as $attr) {
* Converts an RDF dataset to JSON-LD. if (property_exists($quad, $attr) &&
* $quad->{$attr}->type === 'blank node' &&
* @param stdClass $dataset the RDF dataset. strpos($quad->{$attr}->value, '_:c14n') !== 0) {
* @param assoc $options the RDF serialization options. $quad->{$attr}->value = $namer->getName($quad->{$attr}->value);
* }
* @return array the JSON-LD output. }
*/ $normalized[] = $this->toNQuad($quad, property_exists($quad, 'name') ?
protected function _fromRDF($dataset, $options) { $quad->name->value : null);
$default_graph = new stdClass(); }
$graph_map = (object)array('@default' => $default_graph);
// TODO: seems like usages could be replaced by this single map // sort normalized output
$node_references = (object)array(); sort($normalized);
foreach($dataset as $name => $graph) { // handle output format
if(!property_exists($graph_map, $name)) { if (isset($options['format']) && $options['format']) {
$graph_map->{$name} = new stdClass(); if ($options['format'] === 'application/nquads') {
} return implode($normalized);
if($name !== '@default' && !property_exists($default_graph, $name)) { }
$default_graph->{$name} = (object)array('@id' => $name); throw new JsonLdException(
} 'Unknown output format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]);
$node_map = $graph_map->{$name}; }
foreach($graph as $triple) {
// get subject, predicate, object // return RDF dataset
$s = $triple->subject->value; return $this->parseNQuads(implode($normalized));
$p = $triple->predicate->value; }
$o = $triple->object;
/**
if(!property_exists($node_map, $s)) { * Converts an RDF dataset to JSON-LD.
$node_map->{$s} = (object)array('@id' => $s); *
} * @param stdClass $dataset the RDF dataset.
$node = $node_map->{$s}; * @param assoc $options the RDF serialization options.
*
$object_is_id = ($o->type === 'IRI' || $o->type === 'blank node'); * @return array the JSON-LD output.
if($object_is_id && !property_exists($node_map, $o->value)) { */
$node_map->{$o->value} = (object)array('@id' => $o->value); protected function _fromRDF($dataset, $options)
} {
$default_graph = new stdClass();
if($p === self::RDF_TYPE && !$options['useRdfType'] && $object_is_id) { $graph_map = (object) ['@default' => $default_graph];
self::addValue( $referenced_once = (object) [];
$node, '@type', $o->value, array('propertyIsArray' => true));
continue; foreach ($dataset as $name => $graph) {
} if (!property_exists($graph_map, $name)) {
$graph_map->{$name} = new stdClass();
$value = self::_RDFToObject($o, $options['useNativeTypes']); }
self::addValue($node, $p, $value, array('propertyIsArray' => true)); if ($name !== '@default' && !property_exists($default_graph, $name)) {
$default_graph->{$name} = (object) ['@id' => $name];
// object may be an RDF list/partial list node but we can't know }
// easily until all triples are read $node_map = $graph_map->{$name};
if($object_is_id) { foreach ($graph as $triple) {
self::addValue( // get subject, predicate, object
$node_references, $o->value, $node->{'@id'}, array( $s = $triple->subject->value;
'propertyIsArray' => true)); $p = $triple->predicate->value;
$object = $node_map->{$o->value}; $o = $triple->object;
if(!property_exists($object, 'usages')) {
$object->usages = array(); if (!property_exists($node_map, $s)) {
} $node_map->{$s} = (object) ['@id' => $s];
$object->usages[] = (object)array( }
'node' => $node, $node = $node_map->{$s};
'property' => $p,
'value' => $value); $object_is_id = ($o->type === 'IRI' || $o->type === 'blank node');
} if ($object_is_id && !property_exists($node_map, $o->value)) {
} $node_map->{$o->value} = (object) ['@id' => $o->value];
} }
// convert linked lists to @list arrays if ($p === self::RDF_TYPE && !$options['useRdfType'] && $object_is_id) {
foreach($graph_map as $name => $graph_object) { self::addValue(
// no @lists to be converted, continue $node, '@type', $o->value, ['propertyIsArray' => true]);
if(!property_exists($graph_object, self::RDF_NIL)) { continue;
continue; }
}
$value = self::_RDFToObject($o, $options['useNativeTypes']);
// iterate backwards through each RDF list self::addValue($node, $p, $value, ['propertyIsArray' => true]);
$nil = $graph_object->{self::RDF_NIL};
foreach($nil->usages as $usage) { // object may be an RDF list/partial list node but we can't know
$node = $usage->node; // easily until all triples are read
$property = $usage->property; if ($object_is_id) {
$head = $usage->value; if ($o->value === self::RDF_NIL) {
$list = array(); $object = $node_map->{$o->value};
$list_nodes = array(); if (!property_exists($object, 'usages')) {
$object->usages = [];
// ensure node is a well-formed list node; it must: }
// 1. Be referenced only once and used only once in a list. $object->usages[] = (object) [
// 2. Have an array for rdf:first that has 1 item. 'node' => $node,
// 3. Have an array for rdf:rest that has 1 item. 'property' => $p,
// 4. Have no keys other than: @id, usages, rdf:first, rdf:rest, and, 'value' => $value];
// optionally, @type where the value is rdf:List. } else if (property_exists($referenced_once, $o->value)) {
$node_key_count = count(array_keys((array)$node)); // object referenced more than once
while($property === self::RDF_REST && $referenced_once->{$o->value} = false;
property_exists($node_references, $node->{'@id'}) && } else {
count($node_references->{$node->{'@id'}}) === 1 && // track single reference
count($node->usages) === 1 && $referenced_once->{$o->value} = (object) [
property_exists($node, self::RDF_FIRST) && 'node' => $node,
property_exists($node, self::RDF_REST) && 'property' => $p,
is_array($node->{self::RDF_FIRST}) && 'value' => $value];
is_array($node->{self::RDF_REST}) && }
count($node->{self::RDF_FIRST}) === 1 && }
count($node->{self::RDF_REST}) === 1 && }
($node_key_count === 4 || ($node_key_count === 5 && }
property_exists($node, '@type') && is_array($node->{'@type'}) &&
count($node->{'@type'}) === 1 && // convert linked lists to @list arrays
$node->{'@type'}[0] === self::RDF_LIST))) { foreach ($graph_map as $name => $graph_object) {
$list[] = $node->{self::RDF_FIRST}[0]; // no @lists to be converted, continue
$list_nodes[] = $node->{'@id'}; if (!property_exists($graph_object, self::RDF_NIL)) {
continue;
// get next node, moving backwards through list }
$usage = $node->usages[0];
$node = $usage->node; // iterate backwards through each RDF list
$property = $usage->property; $nil = $graph_object->{self::RDF_NIL};
$head = $usage->value; foreach ($nil->usages as $usage) {
$node_key_count = count(array_keys((array)$node)); $node = $usage->node;
$property = $usage->property;
// if node is not a blank node, then list head found $head = $usage->value;
if(strpos($node->{'@id'}, '_:') !== 0) { $list = [];
break; $list_nodes = [];
}
} // ensure node is a well-formed list node; it must:
// 1. Be referenced only once.
// list is nested in another list // 2. Have an array for rdf:first that has 1 item.
if($property === self::RDF_FIRST) { // 3. Have an array for rdf:rest that has 1 item.
// empty list // 4. Have no keys other than: @id, rdf:first, rdf:rest, and,
if($node->{'@id'} === self::RDF_NIL) { // optionally, @type where the value is rdf:List.
// can't convert rdf:nil to a @list object because it would $node_key_count = count(array_keys((array) $node));
// result in a list of lists which isn't supported while ($property === self::RDF_REST &&
continue; property_exists($referenced_once, $node->{'@id'}) &&
} is_object($referenced_once->{$node->{'@id'}}) &&
property_exists($node, self::RDF_FIRST) &&
// preserve list head property_exists($node, self::RDF_REST) &&
$head = $graph_object->{$head->{'@id'}}->{self::RDF_REST}[0]; is_array($node->{self::RDF_FIRST}) &&
array_pop($list); is_array($node->{self::RDF_REST}) &&
array_pop($list_nodes); count($node->{self::RDF_FIRST}) === 1 &&
} count($node->{self::RDF_REST}) === 1 &&
($node_key_count === 3 || ($node_key_count === 4 &&
// transform list into @list object property_exists($node, '@type') && is_array($node->{'@type'}) &&
unset($head->{'@id'}); count($node->{'@type'}) === 1 &&
$head->{'@list'} = array_reverse($list); $node->{'@type'}[0] === self::RDF_LIST))) {
foreach($list_nodes as $list_node) { $list[] = $node->{self::RDF_FIRST}[0];
unset($graph_object->{$list_node}); $list_nodes[] = $node->{'@id'};
}
} // get next node, moving backwards through list
} $usage = $referenced_once->{$node->{'@id'}};
$node = $usage->node;
$result = array(); $property = $usage->property;
$subjects = array_keys((array)$default_graph); $head = $usage->value;
sort($subjects); $node_key_count = count(array_keys((array) $node));
foreach($subjects as $subject) {
$node = $default_graph->{$subject}; // if node is not a blank node, then list head found
if(property_exists($graph_map, $subject)) { if (strpos($node->{'@id'}, '_:') !== 0) {
$node->{'@graph'} = array(); break;
$graph_object = $graph_map->{$subject}; }
$subjects_ = array_keys((array)$graph_object); }
sort($subjects_);
foreach($subjects_ as $subject_) { // list is nested in another list
$node_ = $graph_object->{$subject_}; if ($property === self::RDF_FIRST) {
unset($node_->usages); // empty list
// only add full subjects to top-level if ($node->{'@id'} === self::RDF_NIL) {
if(!self::_isSubjectReference($node_)) { // can't convert rdf:nil to a @list object because it would
$node->{'@graph'}[] = $node_; // result in a list of lists which isn't supported
} continue;
} }
}
unset($node->usages); // preserve list head
// only add full subjects to top-level $head = $graph_object->{$head->{'@id'}}->{self::RDF_REST}[0];
if(!self::_isSubjectReference($node)) { array_pop($list);
$result[] = $node; array_pop($list_nodes);
} }
}
// transform list into @list object
return $result; unset($head->{'@id'});
} $head->{'@list'} = array_reverse($list);
foreach ($list_nodes as $list_node) {
/** unset($graph_object->{$list_node});
* Processes a local context and returns a new active context. }
* }
* @param stdClass $active_ctx the current active context.
* @param mixed $local_ctx the local context to process. unset($nil->usages);
* @param assoc $options the context processing options. }
*
* @return stdClass the new active context. $result = [];
*/ $subjects = array_keys((array) $default_graph);
protected function _processContext($active_ctx, $local_ctx, $options) { sort($subjects);
global $jsonld_cache; foreach ($subjects as $subject) {
$node = $default_graph->{$subject};
// normalize local context to an array if (property_exists($graph_map, $subject)) {
if(is_object($local_ctx) && property_exists($local_ctx, '@context') && $node->{'@graph'} = [];
is_array($local_ctx->{'@context'})) { $graph_object = $graph_map->{$subject};
$local_ctx = $local_ctx->{'@context'}; $subjects_ = array_keys((array) $graph_object);
} sort($subjects_);
$ctxs = self::arrayify($local_ctx); foreach ($subjects_ as $subject_) {
$node_ = $graph_object->{$subject_};
// no contexts in array, clone existing context // only add full subjects to top-level
if(count($ctxs) === 0) { if (!self::_isSubjectReference($node_)) {
return self::_cloneActiveContext($active_ctx); $node->{'@graph'}[] = $node_;
} }
}
// process each context in order, update active context }
// on each iteration to ensure proper caching // only add full subjects to top-level
$rval = $active_ctx; if (!self::_isSubjectReference($node)) {
foreach($ctxs as $ctx) { $result[] = $node;
// reset to initial context }
if($ctx === null) { }
$rval = $active_ctx = $this->_getInitialContext($options);
continue; return $result;
} }
// dereference @context key if present /**
if(is_object($ctx) && property_exists($ctx, '@context')) { * Processes a local context and returns a new active context.
$ctx = $ctx->{'@context'}; *
} * @param stdClass $active_ctx the current active context.
* @param mixed $local_ctx the local context to process.
// context must be an object by now, all URLs retrieved before this call * @param assoc $options the context processing options.
if(!is_object($ctx)) { *
throw new JsonLdException( * @return stdClass the new active context.
'Invalid JSON-LD syntax; @context must be an object.', */
'jsonld.SyntaxError', 'invalid local context', protected function _processContext($active_ctx, $local_ctx, $options)
array('context' => $ctx)); {
} global $jsonld_cache;
// get context from cache if available // normalize local context to an array
if(property_exists($jsonld_cache, 'activeCtx')) { if (is_object($local_ctx) && property_exists($local_ctx, '@context') &&
$cached = $jsonld_cache->activeCtx->get($active_ctx, $ctx); is_array($local_ctx->{'@context'})) {
if($cached) { $local_ctx = $local_ctx->{'@context'};
$rval = $active_ctx = $cached; }
$must_clone = true; $ctxs = self::arrayify($local_ctx);
continue;
} // no contexts in array, clone existing context
} if (count($ctxs) === 0) {
return self::_cloneActiveContext($active_ctx);
// update active context and clone new one before updating }
$active_ctx = $rval;
$rval = self::_cloneActiveContext($rval); // process each context in order, update active context
// on each iteration to ensure proper caching
// define context mappings for keys in local context $rval = $active_ctx;
$defined = new stdClass(); foreach ($ctxs as $ctx) {
// reset to initial context
// handle @base if ($ctx === null) {
if(property_exists($ctx, '@base')) { $rval = $active_ctx = $this->_getInitialContext($options);
$base = $ctx->{'@base'}; continue;
if($base === null) { }
$base = null;
} else if(!is_string($base)) { // dereference @context key if present
throw new JsonLdException( if (is_object($ctx) && property_exists($ctx, '@context')) {
'Invalid JSON-LD syntax; the value of "@base" in a ' . $ctx = $ctx->{'@context'};
'@context must be a string or null.', }
'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx));
} else if($base !== '' && !self::_isAbsoluteIri($base)) { // context must be an object by now, all URLs retrieved before this call
throw new JsonLdException( if (!is_object($ctx)) {
'Invalid JSON-LD syntax; the value of "@base" in a ' . throw new JsonLdException(
'@context must be an absolute IRI or the empty string.', 'Invalid JSON-LD syntax; @context must be an object.', 'jsonld.SyntaxError', 'invalid local context', ['context' => $ctx]);
'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx)); }
}
if($base !== null) { // get context from cache if available
$base = jsonld_parse_url($base); if (property_exists($jsonld_cache, 'activeCtx')) {
} $cached = $jsonld_cache->activeCtx->get($active_ctx, $ctx);
$rval->{'@base'} = $base; if ($cached) {
$defined->{'@base'} = true; $rval = $active_ctx = $cached;
} $must_clone = true;
continue;
// handle @vocab }
if(property_exists($ctx, '@vocab')) { }
$value = $ctx->{'@vocab'};
if($value === null) { // update active context and clone new one before updating
unset($rval->{'@vocab'}); $active_ctx = $rval;
} else if(!is_string($value)) { $rval = self::_cloneActiveContext($rval);
throw new JsonLdException(
'Invalid JSON-LD syntax; the value of "@vocab" in a ' . // define context mappings for keys in local context
'@context must be a string or null.', $defined = new stdClass();
'jsonld.SyntaxError', 'invalid vocab mapping',
array('context' => $ctx)); // handle @base
} else if(!self::_isAbsoluteIri($value)) { if (property_exists($ctx, '@base')) {
throw new JsonLdException( $base = $ctx->{'@base'};
'Invalid JSON-LD syntax; the value of "@vocab" in a ' . if ($base === null) {
'@context must be an absolute IRI.', $base = null;
'jsonld.SyntaxError', 'invalid vocab mapping', } else if (!is_string($base)) {
array('context' => $ctx)); throw new JsonLdException(
} else { 'Invalid JSON-LD syntax; the value of "@base" in a ' .
$rval->{'@vocab'} = $value; '@context must be a string or null.', 'jsonld.SyntaxError', 'invalid base IRI', ['context' => $ctx]);
} } else if ($base !== '' && !self::_isAbsoluteIri($base)) {
$defined->{'@vocab'} = true; throw new JsonLdException(
} 'Invalid JSON-LD syntax; the value of "@base" in a ' .
'@context must be an absolute IRI or the empty string.', 'jsonld.SyntaxError', 'invalid base IRI', ['context' => $ctx]);
// handle @language }
if(property_exists($ctx, '@language')) { if ($base !== null) {
$value = $ctx->{'@language'}; $base = jsonld_parse_url($base);
if($value === null) { }
unset($rval->{'@language'}); $rval->{'@base'} = $base;
} else if(!is_string($value)) { $defined->{'@base'} = true;
throw new JsonLdException( }
'Invalid JSON-LD syntax; the value of "@language" in a ' .
'@context must be a string or null.', // handle @vocab
'jsonld.SyntaxError', 'invalid default language', if (property_exists($ctx, '@vocab')) {
array('context' => $ctx)); $value = $ctx->{'@vocab'};
} else { if ($value === null) {
$rval->{'@language'} = strtolower($value); unset($rval->{'@vocab'});
} } else if (!is_string($value)) {
$defined->{'@language'} = true; throw new JsonLdException(
} 'Invalid JSON-LD syntax; the value of "@vocab" in a ' .
'@context must be a string or null.', 'jsonld.SyntaxError', 'invalid vocab mapping', ['context' => $ctx]);
// process all other keys } else if (!self::_isAbsoluteIri($value)) {
foreach($ctx as $k => $v) { throw new JsonLdException(
$this->_createTermDefinition($rval, $ctx, $k, $defined); 'Invalid JSON-LD syntax; the value of "@vocab" in a ' .
} '@context must be an absolute IRI.', 'jsonld.SyntaxError', 'invalid vocab mapping', ['context' => $ctx]);
} else {
// cache result $rval->{'@vocab'} = $value;
if(property_exists($jsonld_cache, 'activeCtx')) { }
$jsonld_cache->activeCtx->set($active_ctx, $ctx, $rval); $defined->{'@vocab'} = true;
} }
}
// handle @language
return $rval; if (property_exists($ctx, '@language')) {
} $value = $ctx->{'@language'};
if ($value === null) {
/** unset($rval->{'@language'});
* Expands a language map. } else if (!is_string($value)) {
* throw new JsonLdException(
* @param stdClass $language_map the language map to expand. 'Invalid JSON-LD syntax; the value of "@language" in a ' .
* '@context must be a string or null.', 'jsonld.SyntaxError', 'invalid default language', ['context' => $ctx]);
* @return array the expanded language map. } else {
*/ $rval->{'@language'} = strtolower($value);
protected function _expandLanguageMap($language_map) { }
$rval = array(); $defined->{'@language'} = true;
$keys = array_keys((array)$language_map); }
sort($keys);
foreach($keys as $key) { // process all other keys
$values = $language_map->{$key}; foreach ($ctx as $k => $v) {
$values = self::arrayify($values); $this->_createTermDefinition($rval, $ctx, $k, $defined);
foreach($values as $item) { }
if(!is_string($item)) {
throw new JsonLdException( // cache result
'Invalid JSON-LD syntax; language map values must be strings.', if (property_exists($jsonld_cache, 'activeCtx')) {
'jsonld.SyntaxError', 'invalid language map value', $jsonld_cache->activeCtx->set($active_ctx, $ctx, $rval);
array('languageMap', $language_map)); }
} }
$rval[] = (object)array(
'@value' => $item, return $rval;
'@language' => strtolower($key)); }
}
} /**
return $rval; * Expands a language map.
} *
* @param stdClass $language_map the language map to expand.
/** *
* Labels the blank nodes in the given value using the given UniqueNamer. * @return array the expanded language map.
* */
* @param UniqueNamer $namer the UniqueNamer to use. protected function _expandLanguageMap($language_map)
* @param mixed $element the element with blank nodes to rename. {
* $rval = [];
* @return mixed the element. $keys = array_keys((array) $language_map);
*/ sort($keys);
public function _labelBlankNodes($namer, $element) { foreach ($keys as $key) {
if(is_array($element)) { $values = $language_map->{$key};
$length = count($element); $values = self::arrayify($values);
for($i = 0; $i < $length; ++$i) { foreach ($values as $item) {
$element[$i] = $this->_labelBlankNodes($namer, $element[$i]); if ($item === null) {
} continue;
} else if(self::_isList($element)) { }
$element->{'@list'} = $this->_labelBlankNodes( if (!is_string($item)) {
$namer, $element->{'@list'}); throw new JsonLdException(
} else if(is_object($element)) { 'Invalid JSON-LD syntax; language map values must be strings.', 'jsonld.SyntaxError', 'invalid language map value', ['languageMap', $language_map]);
// rename blank node }
if(self::_isBlankNode($element)) { $rval[] = (object) [
$name = null; '@value' => $item,
if(property_exists($element, '@id')) { '@language' => strtolower($key)];
$name = $element->{'@id'}; }
} }
$element->{'@id'} = $namer->getName($name); return $rval;
} }
// recursively apply to all keys /**
$keys = array_keys((array)$element); * Labels the blank nodes in the given value using the given UniqueNamer.
sort($keys); *
foreach($keys as $key) { * @param UniqueNamer $namer the UniqueNamer to use.
if($key !== '@id') { * @param mixed $element the element with blank nodes to rename.
$element->{$key} = $this->_labelBlankNodes($namer, $element->{$key}); *
} * @return mixed the element.
} */
} public function _labelBlankNodes($namer, $element)
{
return $element; if (is_array($element)) {
} $length = count($element);
for ($i = 0; $i < $length; ++$i) {
/** $element[$i] = $this->_labelBlankNodes($namer, $element[$i]);
* Expands the given value by using the coercion and keyword rules in the }
* given context. } else if (self::_isList($element)) {
* $element->{'@list'} = $this->_labelBlankNodes(
* @param stdClass $active_ctx the active context to use. $namer, $element->{'@list'});
* @param string $active_property the property the value is associated with. } else if (is_object($element)) {
* @param mixed $value the value to expand. // rename blank node
* if (self::_isBlankNode($element)) {
* @return mixed the expanded value. $name = null;
*/ if (property_exists($element, '@id')) {
protected function _expandValue($active_ctx, $active_property, $value) { $name = $element->{'@id'};
// nothing to expand }
if($value === null) { $element->{'@id'} = $namer->getName($name);
return null; }
}
// recursively apply to all keys
// special-case expand @id and @type (skips '@id' expansion) $keys = array_keys((array) $element);
$expanded_property = $this->_expandIri( sort($keys);
$active_ctx, $active_property, array('vocab' => true)); foreach ($keys as $key) {
if($expanded_property === '@id') { if ($key !== '@id') {
return $this->_expandIri($active_ctx, $value, array('base' => true)); $element->{$key} = $this->_labelBlankNodes($namer, $element->{$key});
} else if($expanded_property === '@type') { }
return $this->_expandIri( }
$active_ctx, $value, array('vocab' => true, 'base' => true)); }
}
return $element;
// get type definition from context }
$type = self::getContextValue($active_ctx, $active_property, '@type');
/**
// do @id expansion (automatic for @graph) * Expands the given value by using the coercion and keyword rules in the
if($type === '@id' || ($expanded_property === '@graph' && * given context.
is_string($value))) { *
return (object)array('@id' => $this->_expandIri( * @param stdClass $active_ctx the active context to use.
$active_ctx, $value, array('base' => true))); * @param string $active_property the property the value is associated with.
} * @param mixed $value the value to expand.
// do @id expansion w/vocab *
if($type === '@vocab') { * @return mixed the expanded value.
return (object)array('@id' => $this->_expandIri( */
$active_ctx, $value, array('vocab' => true, 'base' => true))); protected function _expandValue($active_ctx, $active_property, $value)
} {
// nothing to expand
// do not expand keyword values if ($value === null) {
if(self::_isKeyword($expanded_property)) { return null;
return $value; }
}
// special-case expand @id and @type (skips '@id' expansion)
$rval = new stdClass(); $expanded_property = $this->_expandIri(
$active_ctx, $active_property, ['vocab' => true]);
// other type if ($expanded_property === '@id') {
if($type !== null) { return $this->_expandIri($active_ctx, $value, ['base' => true]);
$rval->{'@type'} = $type; } else if ($expanded_property === '@type') {
} else if(is_string($value)) { return $this->_expandIri(
// check for language tagging for strings $active_ctx, $value, ['vocab' => true, 'base' => true]);
$language = self::getContextValue( }
$active_ctx, $active_property, '@language');
if($language !== null) { // get type definition from context
$rval->{'@language'} = $language; $type = self::getContextValue($active_ctx, $active_property, '@type');
}
} // do @id expansion (automatic for @graph)
$rval->{'@value'} = $value; if ($type === '@id' || ($expanded_property === '@graph' &&
is_string($value))) {
return $rval; return (object) ['@id' => $this->_expandIri(
} $active_ctx, $value, ['base' => true])];
}
/** // do @id expansion w/vocab
* Creates an array of RDF triples for the given graph. if ($type === '@vocab') {
* return (object) ['@id' => $this->_expandIri(
* @param stdClass $graph the graph to create RDF triples for. $active_ctx, $value, ['vocab' => true, 'base' => true])];
* @param UniqueNamer $namer for assigning bnode names. }
* @param assoc $options the RDF serialization options.
* // do not expand keyword values
* @return array the array of RDF triples for the given graph. if (self::_isKeyword($expanded_property)) {
*/ return $value;
protected function _graphToRDF($graph, $namer, $options) { }
$rval = array();
$rval = new stdClass();
$ids = array_keys((array)$graph);
sort($ids); // other type
foreach($ids as $id) { if ($type !== null) {
$node = $graph->{$id}; $rval->{'@type'} = $type;
if($id === '"') { } else if (is_string($value)) {
$id = ''; // check for language tagging for strings
} $language = self::getContextValue(
$properties = array_keys((array)$node); $active_ctx, $active_property, '@language');
sort($properties); if ($language !== null) {
foreach($properties as $property) { $rval->{'@language'} = $language;
$items = $node->{$property}; }
if($property === '@type') { }
$property = self::RDF_TYPE; $rval->{'@value'} = $value;
} else if(self::_isKeyword($property)) {
continue; return $rval;
} }
foreach($items as $item) { /**
// skip relative IRI subjects and predicates * Creates an array of RDF triples for the given graph.
if(!(self::_isAbsoluteIri($id) && self::_isAbsoluteIri($property))) { *
continue; * @param stdClass $graph the graph to create RDF triples for.
} * @param UniqueNamer $namer for assigning bnode names.
* @param assoc $options the RDF serialization options.
// RDF subject *
$subject = new stdClass(); * @return array the array of RDF triples for the given graph.
$subject->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; */
$subject->value = $id; protected function _graphToRDF($graph, $namer, $options)
{
// RDF predicate $rval = [];
$predicate = new stdClass();
$predicate->type = (strpos($property, '_:') === 0 ? $ids = array_keys((array) $graph);
'blank node' : 'IRI'); sort($ids);
$predicate->value = $property; foreach ($ids as $id) {
$node = $graph->{$id};
// skip bnode predicates unless producing generalized RDF if ($id === '"') {
if($predicate->type === 'blank node' && $id = '';
!$options['produceGeneralizedRdf']) { }
continue; $properties = array_keys((array) $node);
} sort($properties);
foreach ($properties as $property) {
if(self::_isList($item)) { $items = $node->{$property};
// convert @list to triples if ($property === '@type') {
$this->_listToRDF( $property = self::RDF_TYPE;
$item->{'@list'}, $namer, $subject, $predicate, $rval); } else if (self::_isKeyword($property)) {
} else { continue;
// convert value or node object to triple }
$object = $this->_objectToRDF($item);
// skip null objects (they are relative IRIs) foreach ($items as $item) {
if($object) { // skip relative IRI subjects and predicates
$rval[] = (object)array( if (!(self::_isAbsoluteIri($id) && self::_isAbsoluteIri($property))) {
'subject' => $subject, continue;
'predicate' => $predicate, }
'object' => $object);
} // RDF subject
} $subject = new stdClass();
} $subject->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI';
} $subject->value = $id;
}
// RDF predicate
return $rval; $predicate = new stdClass();
} $predicate->type = (strpos($property, '_:') === 0 ?
'blank node' : 'IRI');
/** $predicate->value = $property;
* Converts a @list value into linked list of blank node RDF triples
* (an RDF collection). // skip bnode predicates unless producing generalized RDF
* if ($predicate->type === 'blank node' &&
* @param array $list the @list value. !$options['produceGeneralizedRdf']) {
* @param UniqueNamer $namer for assigning blank node names. continue;
* @param stdClass $subject the subject for the head of the list. }
* @param stdClass $predicate the predicate for the head of the list.
* @param &array $triples the array of triples to append to. if (self::_isList($item)) {
*/ // convert @list to triples
protected function _listToRDF( $this->_listToRDF(
$list, $namer, $subject, $predicate, &$triples) { $item->{'@list'}, $namer, $subject, $predicate, $rval);
$first = (object)array('type' => 'IRI', 'value' => self::RDF_FIRST); } else {
$rest = (object)array('type' => 'IRI', 'value' => self::RDF_REST); // convert value or node object to triple
$nil = (object)array('type' => 'IRI', 'value' => self::RDF_NIL); $object = $this->_objectToRDF($item);
// skip null objects (they are relative IRIs)
foreach($list as $item) { if ($object) {
$blank_node = (object)array( $rval[] = (object) [
'type' => 'blank node', 'value' => $namer->getName()); 'subject' => $subject,
$triples[] = (object)array( 'predicate' => $predicate,
'subject' => $subject, 'object' => $object];
'predicate' => $predicate, }
'object' => $blank_node); }
}
$subject = $blank_node; }
$predicate = $first; }
$object = $this->_objectToRDF($item);
// skip null objects (they are relative IRIs) return $rval;
if($object) { }
$triples[] = (object)array(
'subject' => $subject, /**
'predicate' => $predicate, * Converts a @list value into linked list of blank node RDF triples
'object' => $object); * (an RDF collection).
} *
* @param array $list the @list value.
$predicate = $rest; * @param UniqueNamer $namer for assigning blank node names.
} * @param stdClass $subject the subject for the head of the list.
* @param stdClass $predicate the predicate for the head of the list.
$triples[] = (object)array( * @param &array $triples the array of triples to append to.
'subject' => $subject, 'predicate' => $predicate, 'object' => $nil); */
} protected function _listToRDF(
$list, $namer, $subject, $predicate, &$triples)
/** {
* Converts a JSON-LD value object to an RDF literal or a JSON-LD string or $first = (object) ['type' => 'IRI', 'value' => self::RDF_FIRST];
* node object to an RDF resource. $rest = (object) ['type' => 'IRI', 'value' => self::RDF_REST];
* $nil = (object) ['type' => 'IRI', 'value' => self::RDF_NIL];
* @param mixed $item the JSON-LD value or node object.
* foreach ($list as $item) {
* @return stdClass the RDF literal or RDF resource. $blank_node = (object) [
*/ 'type' => 'blank node', 'value' => $namer->getName()];
protected function _objectToRDF($item) { $triples[] = (object) [
$object = new stdClass(); 'subject' => $subject,
'predicate' => $predicate,
if(self::_isValue($item)) { 'object' => $blank_node];
$object->type = 'literal';
$value = $item->{'@value'}; $subject = $blank_node;
$datatype = property_exists($item, '@type') ? $item->{'@type'} : null; $predicate = $first;
$object = $this->_objectToRDF($item);
// convert to XSD datatypes as appropriate // skip null objects (they are relative IRIs)
if(is_bool($value)) { if ($object) {
$object->value = ($value ? 'true' : 'false'); $triples[] = (object) [
$object->datatype = $datatype ? $datatype : self::XSD_BOOLEAN; 'subject' => $subject,
} else if(is_double($value) || $datatype == self::XSD_DOUBLE) { 'predicate' => $predicate,
// canonical double representation 'object' => $object];
$object->value = preg_replace( }
'/(\d)0*E\+?/', '$1E', sprintf('%1.15E', $value));
$object->datatype = $datatype ? $datatype : self::XSD_DOUBLE; $predicate = $rest;
} else if(is_integer($value)) { }
$object->value = strval($value);
$object->datatype = $datatype ? $datatype : self::XSD_INTEGER; $triples[] = (object) [
} else if(property_exists($item, '@language')) { 'subject' => $subject, 'predicate' => $predicate, 'object' => $nil];
$object->value = $value; }
$object->datatype = $datatype ? $datatype : self::RDF_LANGSTRING;
$object->language = $item->{'@language'}; /**
} else { * Converts a JSON-LD value object to an RDF literal or a JSON-LD string or
$object->value = $value; * node object to an RDF resource.
$object->datatype = $datatype ? $datatype : self::XSD_STRING; *
} * @param mixed $item the JSON-LD value or node object.
} else { *
// convert string/node object to RDF * @return stdClass the RDF literal or RDF resource.
$id = is_object($item) ? $item->{'@id'} : $item; */
$object->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; protected function _objectToRDF($item)
$object->value = $id; {
} $object = new stdClass();
// skip relative IRIs if (self::_isValue($item)) {
if($object->type === 'IRI' && !self::_isAbsoluteIri($object->value)) { $object->type = 'literal';
return null; $value = $item->{'@value'};
} $datatype = property_exists($item, '@type') ? $item->{'@type'} : null;
return $object; // convert to XSD datatypes as appropriate
} if (is_bool($value)) {
$object->value = ($value ? 'true' : 'false');
/** $object->datatype = $datatype ? $datatype : self::XSD_BOOLEAN;
* Converts an RDF triple object to a JSON-LD object. } else if (is_double($value) || $datatype == self::XSD_DOUBLE) {
* // canonical double representation
* @param stdClass $o the RDF triple object to convert. $object->value = preg_replace(
* @param bool $use_native_types true to output native types, false not to. '/(\d)0*E\+?/', '$1E', sprintf('%1.15E', $value));
* $object->datatype = $datatype ? $datatype : self::XSD_DOUBLE;
* @return stdClass the JSON-LD object. } else if (is_integer($value)) {
*/ $object->value = strval($value);
protected function _RDFToObject($o, $use_native_types) { $object->datatype = $datatype ? $datatype : self::XSD_INTEGER;
// convert IRI/blank node object to JSON-LD } else if (property_exists($item, '@language')) {
if($o->type === 'IRI' || $o->type === 'blank node') { $object->value = $value;
return (object)array('@id' => $o->value); $object->datatype = $datatype ? $datatype : self::RDF_LANGSTRING;
} $object->language = $item->{'@language'};
} else {
// convert literal object to JSON-LD $object->value = $value;
$rval = (object)array('@value' => $o->value); $object->datatype = $datatype ? $datatype : self::XSD_STRING;
}
if(property_exists($o, 'language')) { } else {
// add language // convert string/node object to RDF
$rval->{'@language'} = $o->language; $id = is_object($item) ? $item->{'@id'} : $item;
} else { $object->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI';
// add datatype $object->value = $id;
$type = $o->datatype; }
// use native types for certain xsd types
if($use_native_types) { // skip relative IRIs
if($type === self::XSD_BOOLEAN) { if ($object->type === 'IRI' && !self::_isAbsoluteIri($object->value)) {
if($rval->{'@value'} === 'true') { return null;
$rval->{'@value'} = true; }
} else if($rval->{'@value'} === 'false') {
$rval->{'@value'} = false; return $object;
} }
} else if(is_numeric($rval->{'@value'})) {
if($type === self::XSD_INTEGER) { /**
$i = intval($rval->{'@value'}); * Converts an RDF triple object to a JSON-LD object.
if(strval($i) === $rval->{'@value'}) { *
$rval->{'@value'} = $i; * @param stdClass $o the RDF triple object to convert.
} * @param bool $use_native_types true to output native types, false not to.
} else if($type === self::XSD_DOUBLE) { *
$rval->{'@value'} = doubleval($rval->{'@value'}); * @return stdClass the JSON-LD object.
} */
} protected function _RDFToObject($o, $use_native_types)
// do not add native type {
if(!in_array($type, array( // convert IRI/blank node object to JSON-LD
self::XSD_BOOLEAN, self::XSD_INTEGER, self::XSD_DOUBLE, if ($o->type === 'IRI' || $o->type === 'blank node') {
self::XSD_STRING))) { return (object) ['@id' => $o->value];
$rval->{'@type'} = $type; }
}
} else if($type !== self::XSD_STRING) { // convert literal object to JSON-LD
$rval->{'@type'} = $type; $rval = (object) ['@value' => $o->value];
}
} if (property_exists($o, 'language')) {
// add language
return $rval; $rval->{'@language'} = $o->language;
} } else {
// add datatype
/** $type = $o->datatype;
* Recursively flattens the subjects in the given JSON-LD expanded input // use native types for certain xsd types
* into a node map. if ($use_native_types) {
* if ($type === self::XSD_BOOLEAN) {
* @param mixed $input the JSON-LD expanded input. if ($rval->{'@value'} === 'true') {
* @param stdClass $graphs a map of graph name to subject map. $rval->{'@value'} = true;
* @param string $graph the name of the current graph. } else if ($rval->{'@value'} === 'false') {
* @param UniqueNamer $namer the blank node namer. $rval->{'@value'} = false;
* @param mixed $name the name assigned to the current input if it is a bnode. }
* @param mixed $list the list to append to, null for none. } else if (is_numeric($rval->{'@value'})) {
*/ if ($type === self::XSD_INTEGER) {
protected function _createNodeMap( $i = intval($rval->{'@value'});
$input, $graphs, $graph, $namer, $name=null, $list=null) { if (strval($i) === $rval->{'@value'}) {
// recurse through array $rval->{'@value'} = $i;
if(is_array($input)) { }
foreach($input as $e) { } else if ($type === self::XSD_DOUBLE) {
$this->_createNodeMap($e, $graphs, $graph, $namer, null, $list); $rval->{'@value'} = doubleval($rval->{'@value'});
} }
return; }
} // do not add native type
if (!in_array($type, [
// add non-object to list self::XSD_BOOLEAN, self::XSD_INTEGER, self::XSD_DOUBLE,
if(!is_object($input)) { self::XSD_STRING])) {
if($list !== null) { $rval->{'@type'} = $type;
$list[] = $input; }
} } else if ($type !== self::XSD_STRING) {
return; $rval->{'@type'} = $type;
} }
}
// add values to list
if(self::_isValue($input)) { return $rval;
if(property_exists($input, '@type')) { }
$type = $input->{'@type'};
// rename @type blank node /**
if(strpos($type, '_:') === 0) { * Recursively flattens the subjects in the given JSON-LD expanded input
$type = $input->{'@type'} = $namer->getName($type); * into a node map.
} *
} * @param mixed $input the JSON-LD expanded input.
if($list !== null) { * @param stdClass $graphs a map of graph name to subject map.
$list[] = $input; * @param string $graph the name of the current graph.
} * @param UniqueNamer $namer the blank node namer.
return; * @param mixed $name the name assigned to the current input if it is a bnode.
} * @param mixed $list the list to append to, null for none.
*/
// Note: At this point, input must be a subject. protected function _createNodeMap(
$input, $graphs, $graph, $namer, $name = null, $list = null)
// spec requires @type to be named first, so assign names early {
if(property_exists($input, '@type')) { // recurse through array
foreach($input->{'@type'} as $type) { if (is_array($input)) {
if(strpos($type, '_:') === 0) { foreach ($input as $e) {
$namer->getName($type); $this->_createNodeMap($e, $graphs, $graph, $namer, null, $list);
} }
} return;
} }
// get name for subject // add non-object to list
if($name === null) { if (!is_object($input)) {
if(property_exists($input, '@id')) { if ($list !== null) {
$name = $input->{'@id'}; $list[] = $input;
} }
if(self::_isBlankNode($input)) { return;
$name = $namer->getName($name); }
}
} // add values to list
if (self::_isValue($input)) {
// add subject reference to list if (property_exists($input, '@type')) {
if($list !== null) { $type = $input->{'@type'};
$list[] = (object)array('@id' => $name); // rename @type blank node
} if (strpos($type, '_:') === 0) {
$type = $input->{'@type'} = $namer->getName($type);
// create new subject or merge into existing one }
if(!property_exists($graphs, $graph)) { }
$graphs->{$graph} = new stdClass(); if ($list !== null) {
} $list[] = $input;
$subjects = $graphs->{$graph}; }
if(!property_exists($subjects, $name)) { return;
if($name === '') { }
$subjects->{'"'} = new stdClass();
} else { // Note: At this point, input must be a subject.
$subjects->{$name} = new stdClass(); // spec requires @type to be named first, so assign names early
} if (property_exists($input, '@type')) {
} foreach ($input->{'@type'} as $type) {
if($name === '') { if (strpos($type, '_:') === 0) {
$subject = $subjects->{'"'}; $namer->getName($type);
} else { }
$subject = $subjects->{$name}; }
} }
$subject->{'@id'} = $name;
$properties = array_keys((array)$input); // get name for subject
sort($properties); if ($name === null) {
foreach($properties as $property) { if (property_exists($input, '@id')) {
// skip @id $name = $input->{'@id'};
if($property === '@id') { }
continue; if (self::_isBlankNode($input)) {
} $name = $namer->getName($name);
}
// handle reverse properties }
if($property === '@reverse') {
$referenced_node = (object)array('@id' => $name); // add subject reference to list
$reverse_map = $input->{'@reverse'}; if ($list !== null) {
foreach($reverse_map as $reverse_property => $items) { $list[] = (object) ['@id' => $name];
foreach($items as $item) { }
$item_name = null;
if(property_exists($item, '@id')) { // create new subject or merge into existing one
$item_name = $item->{'@id'}; if (!property_exists($graphs, $graph)) {
} $graphs->{$graph} = new stdClass();
if(self::_isBlankNode($item)) { }
$item_name = $namer->getName($item_name); $subjects = $graphs->{$graph};
} if (!property_exists($subjects, $name)) {
$this->_createNodeMap($item, $graphs, $graph, $namer, $item_name); if ($name === '') {
if($item_name === '') { $subjects->{'"'} = new stdClass();
$item_name = '"'; } else {
} $subjects->{$name} = new stdClass();
self::addValue( }
$subjects->{$item_name}, $reverse_property, $referenced_node, }
array('propertyIsArray' => true, 'allowDuplicate' => false)); if ($name === '') {
} $subject = $subjects->{'"'};
} } else {
continue; $subject = $subjects->{$name};
} }
$subject->{'@id'} = $name;
// recurse into graph $properties = array_keys((array) $input);
if($property === '@graph') { sort($properties);
// add graph subjects map entry foreach ($properties as $property) {
if(!property_exists($graphs, $name)) { // skip @id
// FIXME: temporary hack to avoid empty property bug if ($property === '@id') {
if(!$name) { continue;
$name = '"'; }
}
$graphs->{$name} = new stdClass(); // handle reverse properties
} if ($property === '@reverse') {
$g = ($graph === '@merged') ? $graph : $name; $referenced_node = (object) ['@id' => $name];
$this->_createNodeMap( $reverse_map = $input->{'@reverse'};
$input->{$property}, $graphs, $g, $namer, null, null); foreach ($reverse_map as $reverse_property => $items) {
continue; foreach ($items as $item) {
} $item_name = null;
if (property_exists($item, '@id')) {
// copy non-@type keywords $item_name = $item->{'@id'};
if($property !== '@type' && self::_isKeyword($property)) { }
if($property === '@index' && property_exists($subject, '@index')) { if (self::_isBlankNode($item)) {
throw new JsonLdException( $item_name = $namer->getName($item_name);
'Invalid JSON-LD syntax; conflicting @index property detected.', }
'jsonld.SyntaxError', 'conflicting indexes', $this->_createNodeMap($item, $graphs, $graph, $namer, $item_name);
array('subject' => $subject)); if ($item_name === '') {
} $item_name = '"';
$subject->{$property} = $input->{$property}; }
continue; self::addValue(
} $subjects->{$item_name}, $reverse_property, $referenced_node, ['propertyIsArray' => true, 'allowDuplicate' => false]);
}
// iterate over objects }
$objects = $input->{$property}; continue;
}
// if property is a bnode, assign it a new id
if(strpos($property, '_:') === 0) { // recurse into graph
$property = $namer->getName($property); if ($property === '@graph') {
} // add graph subjects map entry
if (!property_exists($graphs, $name)) {
// ensure property is added for empty arrays // FIXME: temporary hack to avoid empty property bug
if(count($objects) === 0) { if (!$name) {
self::addValue( $name = '"';
$subject, $property, array(), array('propertyIsArray' => true)); }
continue; $graphs->{$name} = new stdClass();
} }
foreach($objects as $o) { $g = ($graph === '@merged') ? $graph : $name;
if($property === '@type') { $this->_createNodeMap(
// rename @type blank nodes $input->{$property}, $graphs, $g, $namer, null, null);
$o = (strpos($o, '_:') === 0) ? $namer->getName($o) : $o; continue;
} }
// handle embedded subject or subject reference // copy non-@type keywords
if(self::_isSubject($o) || self::_isSubjectReference($o)) { if ($property !== '@type' && self::_isKeyword($property)) {
// rename blank node @id if ($property === '@index' && property_exists($subject, '@index') &&
$id = property_exists($o, '@id') ? $o->{'@id'} : null; ($input->{'@index'} !== $subject->{'@index'} ||
if(self::_isBlankNode($o)) { $input->{'@index'}->{'@id'} !== $subject->{'@index'}->{'@id'})) {
$id = $namer->getName($id); throw new JsonLdException(
} 'Invalid JSON-LD syntax; conflicting @index property detected.', 'jsonld.SyntaxError', 'conflicting indexes', ['subject' => $subject]);
}
// add reference and recurse $subject->{$property} = $input->{$property};
self::addValue( continue;
$subject, $property, (object)array('@id' => $id), }
array('propertyIsArray' => true, 'allowDuplicate' => false));
$this->_createNodeMap($o, $graphs, $graph, $namer, $id, null); // iterate over objects
} else if(self::_isList($o)) { $objects = $input->{$property};
// handle @list
$_list = new ArrayObject(); // if property is a bnode, assign it a new id
$this->_createNodeMap( if (strpos($property, '_:') === 0) {
$o->{'@list'}, $graphs, $graph, $namer, $name, $_list); $property = $namer->getName($property);
$o = (object)array('@list' => (array)$_list); }
self::addValue(
$subject, $property, $o, // ensure property is added for empty arrays
array('propertyIsArray' => true, 'allowDuplicate' => false)); if (count($objects) === 0) {
} else { self::addValue(
// handle @value $subject, $property, [], ['propertyIsArray' => true]);
$this->_createNodeMap($o, $graphs, $graph, $namer, $name, null); continue;
self::addValue( }
$subject, $property, $o, foreach ($objects as $o) {
array('propertyIsArray' => true, 'allowDuplicate' => false)); if ($property === '@type') {
} // rename @type blank nodes
} $o = (strpos($o, '_:') === 0) ? $namer->getName($o) : $o;
} }
}
// handle embedded subject or subject reference
/** if (self::_isSubject($o) || self::_isSubjectReference($o)) {
* Frames subjects according to the given frame. // rename blank node @id
* $id = property_exists($o, '@id') ? $o->{'@id'} : null;
* @param stdClass $state the current framing state. if (self::_isBlankNode($o)) {
* @param array $subjects the subjects to filter. $id = $namer->getName($id);
* @param array $frame the frame. }
* @param mixed $parent the parent subject or top-level array.
* @param mixed $property the parent property, initialized to null. // add reference and recurse
*/ self::addValue(
protected function _matchFrame( $subject, $property, (object) ['@id' => $id], ['propertyIsArray' => true, 'allowDuplicate' => false]);
$state, $subjects, $frame, $parent, $property) { $this->_createNodeMap($o, $graphs, $graph, $namer, $id, null);
// validate the frame } else if (self::_isList($o)) {
$this->_validateFrame($state, $frame); // handle @list
$frame = $frame[0]; $_list = new ArrayObject();
$this->_createNodeMap(
// filter out subjects that match the frame $o->{'@list'}, $graphs, $graph, $namer, $name, $_list);
$matches = $this->_filterSubjects($state, $subjects, $frame); $o = (object) ['@list' => (array) $_list];
self::addValue(
// get flags for current frame $subject, $property, $o, ['propertyIsArray' => true, 'allowDuplicate' => false]);
$options = $state->options; } else {
$embed_on = $this->_getFrameFlag($frame, $options, 'embed'); // handle @value
$explicit_on = $this->_getFrameFlag($frame, $options, 'explicit'); $this->_createNodeMap($o, $graphs, $graph, $namer, $name, null);
self::addValue(
// add matches to output $subject, $property, $o, ['propertyIsArray' => true, 'allowDuplicate' => false]);
foreach($matches as $id => $subject) { }
/* Note: In order to treat each top-level match as a compartmentalized }
result, create an independent copy of the embedded subjects map when the }
property is null, which only occurs at the top-level. */ }
if($property === null) {
$state->embeds = new stdClass(); /**
} * Frames subjects according to the given frame.
*
// start output * @param stdClass $state the current framing state.
$output = new stdClass(); * @param array $subjects the subjects to filter.
$output->{'@id'} = $id; * @param array $frame the frame.
* @param mixed $parent the parent subject or top-level array.
// prepare embed meta info * @param mixed $property the parent property, initialized to null.
$embed = (object)array('parent' => $parent, 'property' => $property); */
protected function _matchFrame(
// if embed is on and there is an existing embed $state, $subjects, $frame, $parent, $property)
if($embed_on && property_exists($state->embeds, $id)) { {
// only overwrite an existing embed if it has already been added to its // validate the frame
// parent -- otherwise its parent is somewhere up the tree from this $this->_validateFrame($frame);
// embed and the embed would occur twice once the tree is added $frame = $frame[0];
$embed_on = false;
// get flags for current frame
// existing embed's parent is an array $options = $state->options;
$existing = $state->embeds->{$id}; $flags = [
if(is_array($existing->parent)) { 'embed' => $this->_getFrameFlag($frame, $options, 'embed'),
foreach($existing->parent as $p) { 'explicit' => $this->_getFrameFlag($frame, $options, 'explicit'),
if(self::compareValues($output, $p)) { 'requireAll' => $this->_getFrameFlag($frame, $options, 'requireAll')];
$embed_on = true;
break; // filter out subjects that match the frame
} $matches = $this->_filterSubjects($state, $subjects, $frame, $flags);
}
} else if(self::hasValue( // add matches to output
$existing->parent, $existing->property, $output)) { foreach ($matches as $id => $subject) {
// existing embed's parent is an object if ($flags['embed'] === '@link' && property_exists($state->link, $id)) {
$embed_on = true; // TODO: may want to also match an existing linked subject against
} // the current frame ... so different frames could produce different
// subjects that are only shared in-memory when the frames are the same
// existing embed has already been added, so allow an overwrite // add existing linked subject
if($embed_on) { $this->_addFrameOutput($parent, $property, $state->link->{$id});
$this->_removeEmbed($state, $id); continue;
} }
}
/* Note: In order to treat each top-level match as a compartmentalized
// not embedding, add output without any other properties result, clear the unique embedded subjects map when the property is null,
if(!$embed_on) { which only occurs at the top-level. */
$this->_addFrameOutput($state, $parent, $property, $output); if ($property === null) {
} else { $state->uniqueEmbeds = new stdClass();
// add embed meta info }
$state->embeds->{$id} = $embed;
// start output for subject
// iterate over subject properties $output = new stdClass();
$props = array_keys((array)$subject); $output->{'@id'} = $id;
sort($props); $state->link->{$id} = $output;
foreach($props as $prop) {
// copy keywords to output // if embed is @never or if a circular reference would be created by an
if(self::_isKeyword($prop)) { // embed, the subject cannot be embedded, just add the reference;
$output->{$prop} = self::copy($subject->{$prop}); // note that a circular reference won't occur when the embed flag is
continue; // `@link` as the above check will short-circuit before reaching this point
} if ($flags['embed'] === '@never' ||
$this->_createsCircularReference($subject, $state->subjectStack)) {
// if property isn't in the frame $this->_addFrameOutput($parent, $property, $output);
if(!property_exists($frame, $prop)) { continue;
// if explicit is off, embed values }
if(!$explicit_on) {
$this->_embedValues($state, $subject, $prop, $output); // if only the last match should be embedded
} if ($flags['embed'] === '@last') {
continue; // remove any existing embed
} if (property_exists($state->uniqueEmbeds, $id)) {
$this->_removeEmbed($state, $id);
// add objects }
$objects = $subject->{$prop}; $state->uniqueEmbeds->{$id} = [
foreach($objects as $o) { 'parent' => $parent, 'property' => $property];
// recurse into list }
if(self::_isList($o)) {
// add empty list // push matching subject onto stack to enable circular embed checks
$list = (object)array('@list' => array()); $state->subjectStack[] = $subject;
$this->_addFrameOutput($state, $output, $prop, $list);
// iterate over subject properties
// add list objects $props = array_keys((array) $subject);
$src = $o->{'@list'}; sort($props);
foreach($src as $o) { foreach ($props as $prop) {
if(self::_isSubjectReference($o)) { // copy keywords to output
// recurse into subject reference if (self::_isKeyword($prop)) {
$this->_matchFrame( $output->{$prop} = self::copy($subject->{$prop});
$state, array($o->{'@id'}), $frame->{$prop}[0]->{'@list'}, continue;
$list, '@list'); }
} else {
// include other values automatically // explicit is on and property isn't in the frame, skip processing
$this->_addFrameOutput( if ($flags['explicit'] && !property_exists($frame, $prop)) {
$state, $list, '@list', self::copy($o)); continue;
} }
}
continue; // add objects
} $objects = $subject->{$prop};
foreach ($objects as $o) {
if(self::_isSubjectReference($o)) { // recurse into list
// recurse into subject reference if (self::_isList($o)) {
$this->_matchFrame( // add empty list
$state, array($o->{'@id'}), $frame->{$prop}, $output, $prop); $list = (object) ['@list' => []];
} else { $this->_addFrameOutput($output, $prop, $list);
// include other values automatically
$this->_addFrameOutput($state, $output, $prop, self::copy($o)); // add list objects
} $src = $o->{'@list'};
} foreach ($src as $o) {
} if (self::_isSubjectReference($o)) {
// recurse into subject reference
// handle defaults $subframe = (property_exists($frame, $prop) ?
$props = array_keys((array)$frame); $frame->{$prop}[0]->{'@list'} :
sort($props); $this->_createImplicitFrame($flags));
foreach($props as $prop) { $this->_matchFrame(
// skip keywords $state, [$o->{'@id'}], $subframe, $list, '@list');
if(self::_isKeyword($prop)) { } else {
continue; // include other values automatically
} $this->_addFrameOutput($list, '@list', self::copy($o));
}
// if omit default is off, then include default values for properties }
// that appear in the next frame but are not in the matching subject continue;
$next = $frame->{$prop}[0]; }
$omit_default_on = $this->_getFrameFlag(
$next, $options, 'omitDefault'); if (self::_isSubjectReference($o)) {
if(!$omit_default_on && !property_exists($output, $prop)) { // recurse into subject reference
$preserve = '@null'; $subframe = (property_exists($frame, $prop) ?
if(property_exists($next, '@default')) { $frame->{$prop} : $this->_createImplicitFrame($flags));
$preserve = self::copy($next->{'@default'}); $this->_matchFrame(
} $state, [$o->{'@id'}], $subframe, $output, $prop);
$preserve = self::arrayify($preserve); } else {
$output->{$prop} = array((object)array('@preserve' => $preserve)); // include other values automatically
} $this->_addFrameOutput($output, $prop, self::copy($o));
} }
}
// add output to parent }
$this->_addFrameOutput($state, $parent, $property, $output);
} // handle defaults
} $props = array_keys((array) $frame);
} sort($props);
foreach ($props as $prop) {
/** // skip keywords
* Gets the frame flag value for the given flag name. if (self::_isKeyword($prop)) {
* continue;
* @param stdClass $frame the frame. }
* @param stdClass $options the framing options.
* @param string $name the flag name. // if omit default is off, then include default values for properties
* // that appear in the next frame but are not in the matching subject
* @return mixed $the flag value. $next = $frame->{$prop}[0];
*/ $omit_default_on = $this->_getFrameFlag(
protected function _getFrameFlag($frame, $options, $name) { $next, $options, 'omitDefault');
$flag = "@$name"; if (!$omit_default_on && !property_exists($output, $prop)) {
return (property_exists($frame, $flag) ? $preserve = '@null';
$frame->{$flag}[0] : $options[$name]); if (property_exists($next, '@default')) {
} $preserve = self::copy($next->{'@default'});
}
/** $preserve = self::arrayify($preserve);
* Validates a JSON-LD frame, throwing an exception if the frame is invalid. $output->{$prop} = [(object) ['@preserve' => $preserve]];
* }
* @param stdClass $state the current frame state. }
* @param array $frame the frame to validate.
*/ // add output to parent
protected function _validateFrame($state, $frame) { $this->_addFrameOutput($parent, $property, $output);
if(!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) {
throw new JsonLdException( // pop matching subject from circular ref-checking stack
'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.', array_pop($state->subjectStack);
'jsonld.SyntaxError', null, array('frame' => $frame)); }
} }
}
/**
/** * Creates an implicit frame when recursing through subject matches. If
* Returns a map of all of the subjects that match a parsed frame. * a frame doesn't have an explicit frame for a particular property, then
* * a wildcard child frame will be created that uses the same flags that the
* @param stdClass $state the current framing state. * parent frame used.
* @param array $subjects the set of subjects to filter. *
* @param stdClass $frame the parsed frame. * @param assoc flags the current framing flags.
* *
* @return stdClass all of the matched subjects. * @return array the implicit frame.
*/ */
protected function _filterSubjects($state, $subjects, $frame) { function _createImplicitFrame($flags)
$rval = new stdClass(); {
sort($subjects); $frame = new stdClass();
foreach($subjects as $id) { foreach ($flags as $key => $value) {
$subject = $state->subjects->{$id}; $frame->{'@' . $key} = [$flags[$key]];
if($this->_filterSubject($subject, $frame)) { }
$rval->{$id} = $subject; return [$frame];
} }
}
return $rval; /**
} * Checks the current subject stack to see if embedding the given subject
* would cause a circular reference.
/** *
* Returns true if the given subject matches the given frame. * @param stdClass subject_to_embed the subject to embed.
* * @param assoc subject_stack the current stack of subjects.
* @param stdClass $subject the subject to check. *
* @param stdClass $frame the frame to check. * @return bool true if a circular reference would be created, false if not.
* */
* @return bool true if the subject matches, false if not. function _createsCircularReference($subject_to_embed, $subject_stack)
*/ {
protected function _filterSubject($subject, $frame) { for ($i = count($subject_stack) - 1; $i >= 0; --$i) {
// check @type (object value means 'any' type, fall through to ducktyping) if ($subject_stack[$i]->{'@id'} === $subject_to_embed->{'@id'}) {
if(property_exists($frame, '@type') && return true;
!(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) { }
$types = $frame->{'@type'}; }
foreach($types as $type) { return false;
// any matching @type is a match }
if(self::hasValue($subject, '@type', $type)) {
return true; /**
} * Gets the frame flag value for the given flag name.
} *
return false; * @param stdClass $frame the frame.
} * @param stdClass $options the framing options.
* @param string $name the flag name.
// check ducktype *
foreach($frame as $k => $v) { * @return mixed $the flag value.
// only not a duck if @id or non-keyword isn't in subject */
if(($k === '@id' || !self::_isKeyword($k)) && protected function _getFrameFlag($frame, $options, $name)
!property_exists($subject, $k)) { {
return false; $flag = "@$name";
} $rval = (property_exists($frame, $flag) ?
} $frame->{$flag}[0] : $options[$name]);
return true; if ($name === 'embed') {
} // default is "@last"
// backwards-compatibility support for "embed" maps:
/** // true => "@last"
* Embeds values for the given subject and property into the given output // false => "@never"
* during the framing algorithm. if ($rval === true) {
* $rval = '@last';
* @param stdClass $state the current framing state. } else if ($rval === false) {
* @param stdClass $subject the subject. $rval = '@never';
* @param string $property the property. } else if ($rval !== '@always' && $rval !== '@never' &&
* @param mixed $output the output. $rval !== '@link') {
*/ $rval = '@last';
protected function _embedValues($state, $subject, $property, $output) { }
// embed subject properties in output }
$objects = $subject->{$property}; return $rval;
foreach($objects as $o) { }
// recurse into @list
if(self::_isList($o)) { /**
$list = (object)array('@list' => new ArrayObject()); * Validates a JSON-LD frame, throwing an exception if the frame is invalid.
$this->_addFrameOutput($state, $output, $property, $list); *
$this->_embedValues($state, $o, '@list', $list->{'@list'}); * @param array $frame the frame to validate.
$list->{'@list'} = (array)$list->{'@list'}; */
return; protected function _validateFrame($frame)
} {
if (!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) {
// handle subject reference throw new JsonLdException(
if(self::_isSubjectReference($o)) { 'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.', 'jsonld.SyntaxError', null, ['frame' => $frame]);
$id = $o->{'@id'}; }
}
// embed full subject if isn't already embedded
if(!property_exists($state->embeds, $id)) { /**
// add embed * Returns a map of all of the subjects that match a parsed frame.
$embed = (object)array('parent' => $output, 'property' => $property); *
$state->embeds->{$id} = $embed; * @param stdClass $state the current framing state.
* @param array $subjects the set of subjects to filter.
// recurse into subject * @param stdClass $frame the parsed frame.
$o = new stdClass(); * @param assoc $flags the frame flags.
$s = $state->subjects->{$id}; *
foreach($s as $prop => $v) { * @return stdClass all of the matched subjects.
// copy keywords */
if(self::_isKeyword($prop)) { protected function _filterSubjects($state, $subjects, $frame, $flags)
$o->{$prop} = self::copy($v); {
continue; $rval = new stdClass();
} sort($subjects);
$this->_embedValues($state, $s, $prop, $o); foreach ($subjects as $id) {
} $subject = $state->subjects->{$id};
} if ($this->_filterSubject($subject, $frame, $flags)) {
$this->_addFrameOutput($state, $output, $property, $o); $rval->{$id} = $subject;
} else { }
// copy non-subject value }
$this->_addFrameOutput($state, $output, $property, self::copy($o)); return $rval;
} }
}
} /**
* Returns true if the given subject matches the given frame.
/** *
* Removes an existing embed. * @param stdClass $subject the subject to check.
* * @param stdClass $frame the frame to check.
* @param stdClass $state the current framing state. * @param assoc $flags the frame flags.
* @param string $id the @id of the embed to remove. *
*/ * @return bool true if the subject matches, false if not.
protected function _removeEmbed($state, $id) { */
// get existing embed protected function _filterSubject($subject, $frame, $flags)
$embeds = $state->embeds; {
$embed = $embeds->{$id}; // check @type (object value means 'any' type, fall through to ducktyping)
$property = $embed->property; if (property_exists($frame, '@type') &&
!(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) {
// create reference to replace embed $types = $frame->{'@type'};
$subject = (object)array('@id' => id); foreach ($types as $type) {
// any matching @type is a match
// remove existing embed if (self::hasValue($subject, '@type', $type)) {
if(is_array($embed->parent)) { return true;
// replace subject with reference }
foreach($embed->parent as $i => $parent) { }
if(self::compareValues($parent, $subject)) { return false;
$embed->parent[$i] = $subject; }
break;
} // check ducktype
} $wildcard = true;
} else { $matches_some = false;
// replace subject with reference foreach ($frame as $k => $v) {
$use_array = is_array($embed->parent->{$property}); if (self::_isKeyword($k)) {
self::removeValue($embed->parent, $property, $subject, // skip non-@id and non-@type
array('propertyIsArray' => $use_array)); if ($k !== '@id' && $k !== '@type') {
self::addValue($embed->parent, $property, $subject, continue;
array('propertyIsArray' => $use_array)); }
} $wildcard = false;
// recursively remove dependent dangling embeds // check @id for a specific @id value
$removeDependents = function($id) { if ($k === '@id' && is_string($v)) {
// get embed keys as a separate array to enable deleting keys in map if (!property_exists($subject, $k) || $subject->{$k} !== $v) {
$ids = array_keys((array)$embeds); return false;
foreach($ids as $next) { }
if(property_exists($embeds, $next) && $matches_some = true;
is_object($embeds->{$next}->parent) && continue;
$embeds->{$next}->parent->{'@id'} === $id) { }
unset($embeds->{$next}); }
$removeDependents($next);
} $wildcard = false;
}
}; if (property_exists($subject, $k)) {
$removeDependents($id); // $v === [] means do not match if property is present
} if (is_array($v) && count($v) === 0) {
return false;
/** }
* Adds framing output to the given parent. $matches_some = true;
* continue;
* @param stdClass $state the current framing state. }
* @param mixed $parent the parent to add to.
* @param string $property the parent property. // all properties must match to be a duck unless a @default is specified
* @param mixed $output the output to add. $has_default = (is_array($v) && count($v) === 1 && is_object($v[0]) &&
*/ property_exists($v[0], '@default'));
protected function _addFrameOutput($state, $parent, $property, $output) { if ($flags['requireAll'] && !$has_default) {
if(is_object($parent) && !($parent instanceof ArrayObject)) { return false;
self::addValue( }
$parent, $property, $output, array('propertyIsArray' => true)); }
} else {
$parent[] = $output; // return true if wildcard or subject matches some properties
} return $wildcard || $matches_some;
} }
/** /**
* Removes the @preserve keywords as the last step of the framing algorithm. * Removes an existing embed.
* *
* @param stdClass $ctx the active context used to compact the input. * @param stdClass $state the current framing state.
* @param mixed $input the framed, compacted output. * @param string $id the @id of the embed to remove.
* @param assoc $options the compaction options used. */
* protected function _removeEmbed($state, $id)
* @return mixed the resulting output. {
*/ // get existing embed
protected function _removePreserve($ctx, $input, $options) { $embeds = $state->uniqueEmbeds;
// recurse through arrays $embed = $embeds->{$id};
if(is_array($input)) { $property = $embed['property'];
$output = array();
foreach($input as $e) { // create reference to replace embed
$result = $this->_removePreserve($ctx, $e, $options); $subject = (object) ['@id' => $id];
// drop nulls from arrays
if($result !== null) { // remove existing embed
$output[] = $result; if (is_array($embed->parent)) {
} // replace subject with reference
} foreach ($embed->parent as $i => $parent) {
$input = $output; if (self::compareValues($parent, $subject)) {
} else if(is_object($input)) { $embed->parent[$i] = $subject;
// remove @preserve break;
if(property_exists($input, '@preserve')) { }
if($input->{'@preserve'} === '@null') { }
return null; } else {
} // replace subject with reference
return $input->{'@preserve'}; $use_array = is_array($embed->parent->{$property});
} self::removeValue($embed->parent, $property, $subject, ['propertyIsArray' => $use_array]);
self::addValue($embed->parent, $property, $subject, ['propertyIsArray' => $use_array]);
// skip @values }
if(self::_isValue($input)) {
return $input; // recursively remove dependent dangling embeds
} $removeDependents = function($id) {
// get embed keys as a separate array to enable deleting keys in map
// recurse through @lists $ids = array_keys((array) $embeds);
if(self::_isList($input)) { foreach ($ids as $next) {
$input->{'@list'} = $this->_removePreserve( if (property_exists($embeds, $next) &&
$ctx, $input->{'@list'}, $options); is_object($embeds->{$next}->parent) &&
return $input; $embeds->{$next}->parent->{'@id'} === $id) {
} unset($embeds->{$next});
$removeDependents($next);
// recurse through properties }
foreach($input as $prop => $v) { }
$result = $this->_removePreserve($ctx, $v, $options); };
$container = self::getContextValue($ctx, $prop, '@container'); $removeDependents($id);
if($options['compactArrays'] && }
is_array($result) && count($result) === 1 &&
$container !== '@set' && $container !== '@list') { /**
$result = $result[0]; * Adds framing output to the given parent.
} *
$input->{$prop} = $result; * @param mixed $parent the parent to add to.
} * @param string $property the parent property.
} * @param mixed $output the output to add.
return $input; */
} protected function _addFrameOutput($parent, $property, $output)
{
/** if (is_object($parent) && !($parent instanceof ArrayObject)) {
* Compares two RDF triples for equality. self::addValue(
* $parent, $property, $output, ['propertyIsArray' => true]);
* @param stdClass $t1 the first triple. } else {
* @param stdClass $t2 the second triple. $parent[] = $output;
* }
* @return true if the triples are the same, false if not. }
*/
protected static function _compareRDFTriples($t1, $t2) { /**
foreach(array('subject', 'predicate', 'object') as $attr) { * Removes the @preserve keywords as the last step of the framing algorithm.
if($t1->{$attr}->type !== $t2->{$attr}->type || *
$t1->{$attr}->value !== $t2->{$attr}->value) { * @param stdClass $ctx the active context used to compact the input.
return false; * @param mixed $input the framed, compacted output.
} * @param assoc $options the compaction options used.
} *
if(property_exists($t1->object, 'language') !== * @return mixed the resulting output.
property_exists($t1->object, 'language')) { */
return false; protected function _removePreserve($ctx, $input, $options)
} {
if(property_exists($t1->object, 'language') && // recurse through arrays
$t1->object->language !== $t2->object->language) { if (is_array($input)) {
return false; $output = [];
} foreach ($input as $e) {
if(property_exists($t1->object, 'datatype') && $result = $this->_removePreserve($ctx, $e, $options);
$t1->object->datatype !== $t2->object->datatype) { // drop nulls from arrays
return false; if ($result !== null) {
} $output[] = $result;
return true; }
} }
$input = $output;
/** } else if (is_object($input)) {
* Hashes all of the quads about a blank node. // remove @preserve
* if (property_exists($input, '@preserve')) {
* @param string $id the ID of the bnode to hash quads for. if ($input->{'@preserve'} === '@null') {
* @param stdClass $bnodes the mapping of bnodes to quads. return null;
* @param UniqueNamer $namer the canonical bnode namer. }
* return $input->{'@preserve'};
* @return string the new hash. }
*/
protected function _hashQuads($id, $bnodes, $namer) { // skip @values
// return cached hash if (self::_isValue($input)) {
if(property_exists($bnodes->{$id}, 'hash')) { return $input;
return $bnodes->{$id}->hash; }
}
// recurse through @lists
// serialize all of bnode's quads if (self::_isList($input)) {
$quads = $bnodes->{$id}->quads; $input->{'@list'} = $this->_removePreserve(
$nquads = array(); $ctx, $input->{'@list'}, $options);
foreach($quads as $quad) { return $input;
$nquads[] = $this->toNQuad($quad, property_exists($quad, 'name') ? }
$quad->name->value : null, $id);
} // handle in-memory linked nodes
$id_alias = $this->_compactIri($ctx, '@id');
// sort serialized quads if (property_exists($input, $id_alias)) {
sort($nquads); $id = $input->{$id_alias};
if (isset($options['link'][$id])) {
// cache and return hashed quads $idx = array_search($input, $options['link'][$id]);
$hash = $bnodes->{$id}->hash = sha1(implode($nquads)); if ($idx === false) {
return $hash; // prevent circular visitation
} $options['link'][$id][] = $input;
} else {
/** // already visited
* Produces a hash for the paths of adjacent bnodes for a bnode, return $options['link'][$id][$idx];
* incorporating all information about its subgraph of bnodes. This }
* method will recursively pick adjacent bnode permutations that produce the } else {
* lexicographically-least 'path' serializations. // prevent circular visitation
* $options['link'][$id] = [$input];
* @param string $id the ID of the bnode to hash paths for. }
* @param stdClass $bnodes the map of bnode quads. }
* @param UniqueNamer $namer the canonical bnode namer.
* @param UniqueNamer $path_namer the namer used to assign names to adjacent // recurse through properties
* bnodes. foreach ($input as $prop => $v) {
* $result = $this->_removePreserve($ctx, $v, $options);
* @return stdClass the hash and path namer used. $container = self::getContextValue($ctx, $prop, '@container');
*/ if ($options['compactArrays'] &&
protected function _hashPaths($id, $bnodes, $namer, $path_namer) { is_array($result) && count($result) === 1 &&
// create SHA-1 digest $container !== '@set' && $container !== '@list') {
$md = hash_init('sha1'); $result = $result[0];
}
// group adjacent bnodes by hash, keep properties and references separate $input->{$prop} = $result;
$groups = new stdClass(); }
$quads = $bnodes->{$id}->quads; }
foreach($quads as $quad) { return $input;
// get adjacent bnode }
$bnode = $this->_getAdjacentBlankNodeName($quad->subject, $id);
if($bnode !== null) { /**
// normal property * Compares two RDF triples for equality.
$direction = 'p'; *
} else { * @param stdClass $t1 the first triple.
$bnode = $this->_getAdjacentBlankNodeName($quad->object, $id); * @param stdClass $t2 the second triple.
if($bnode !== null) { *
// reverse property * @return true if the triples are the same, false if not.
$direction = 'r'; */
} protected static function _compareRDFTriples($t1, $t2)
} {
if($bnode !== null) { foreach (['subject', 'predicate', 'object'] as $attr) {
// get bnode name (try canonical, path, then hash) if ($t1->{$attr}->type !== $t2->{$attr}->type ||
if($namer->isNamed($bnode)) { $t1->{$attr}->value !== $t2->{$attr}->value) {
$name = $namer->getName($bnode); return false;
} else if($path_namer->isNamed($bnode)) { }
$name = $path_namer->getName($bnode); }
} else { if (property_exists($t1->object, 'language') !== property_exists($t1->object, 'language')) {
$name = $this->_hashQuads($bnode, $bnodes, $namer); return false;
} }
if (property_exists($t1->object, 'language') &&
// hash direction, property, and bnode name/hash $t1->object->language !== $t2->object->language) {
$group_md = hash_init('sha1'); return false;
hash_update($group_md, $direction); }
hash_update($group_md, $quad->predicate->value); if (property_exists($t1->object, 'datatype') &&
hash_update($group_md, $name); $t1->object->datatype !== $t2->object->datatype) {
$group_hash = hash_final($group_md); return false;
}
// add bnode to hash group return true;
if(property_exists($groups, $group_hash)) { }
$groups->{$group_hash}[] = $bnode;
} else { /**
$groups->{$group_hash} = array($bnode); * Hashes all of the quads about a blank node.
} *
} * @param string $id the ID of the bnode to hash quads for.
} * @param stdClass $bnodes the mapping of bnodes to quads.
* @param UniqueNamer $namer the canonical bnode namer.
// iterate over groups in sorted hash order *
$group_hashes = array_keys((array)$groups); * @return string the new hash.
sort($group_hashes); */
foreach($group_hashes as $group_hash) { protected function _hashQuads($id, $bnodes, $namer)
// digest group hash {
hash_update($md, $group_hash); // return cached hash
if (property_exists($bnodes->{$id}, 'hash')) {
// choose a path and namer from the permutations return $bnodes->{$id}->hash;
$chosen_path = null; }
$chosen_namer = null;
$permutator = new Permutator($groups->{$group_hash}); // serialize all of bnode's quads
while($permutator->hasNext()) { $quads = $bnodes->{$id}->quads;
$permutation = $permutator->next(); $nquads = [];
$path_namer_copy = clone $path_namer; foreach ($quads as $quad) {
$nquads[] = $this->toNQuad($quad, property_exists($quad, 'name') ?
// build adjacent path $quad->name->value : null, $id);
$path = ''; }
$skipped = false;
$recurse = array(); // sort serialized quads
foreach($permutation as $bnode) { sort($nquads);
// use canonical name if available
if($namer->isNamed($bnode)) { // cache and return hashed quads
$path .= $namer->getName($bnode); $hash = $bnodes->{$id}->hash = hash('sha256', implode($nquads));
} else { return $hash;
// recurse if bnode isn't named in the path yet }
if(!$path_namer_copy->isNamed($bnode)) {
$recurse[] = $bnode; /**
} * Produces a hash for the paths of adjacent bnodes for a bnode,
$path .= $path_namer_copy->getName($bnode); * incorporating all information about its subgraph of bnodes. This
} * method will recursively pick adjacent bnode permutations that produce the
* lexicographically-least 'path' serializations.
// skip permutation if path is already >= chosen path *
if($chosen_path !== null && strlen($path) >= strlen($chosen_path) && * @param string $id the ID of the bnode to hash paths for.
$path > $chosen_path) { * @param stdClass $bnodes the map of bnode quads.
$skipped = true; * @param UniqueNamer $namer the canonical bnode namer.
break; * @param UniqueNamer $path_namer the namer used to assign names to adjacent
} * bnodes.
} *
* @return stdClass the hash and path namer used.
// recurse */
if(!$skipped) { protected function _hashPaths($id, $bnodes, $namer, $path_namer)
foreach($recurse as $bnode) { {
$result = $this->_hashPaths( // create SHA-1 digest
$bnode, $bnodes, $namer, $path_namer_copy); $md = hash_init('sha256');
$path .= $path_namer_copy->getName($bnode);
$path .= "<{$result->hash}>"; // group adjacent bnodes by hash, keep properties and references separate
$path_namer_copy = $result->pathNamer; $groups = new stdClass();
$quads = $bnodes->{$id}->quads;
// skip permutation if path is already >= chosen path foreach ($quads as $quad) {
if($chosen_path !== null && // get adjacent bnode
strlen($path) >= strlen($chosen_path) && $path > $chosen_path) { $bnode = $this->_getAdjacentBlankNodeName($quad->subject, $id);
$skipped = true; if ($bnode !== null) {
break; // normal property
} $direction = 'p';
} } else {
} $bnode = $this->_getAdjacentBlankNodeName($quad->object, $id);
if ($bnode !== null) {
if(!$skipped && ($chosen_path === null || $path < $chosen_path)) { // reverse property
$chosen_path = $path; $direction = 'r';
$chosen_namer = $path_namer_copy; }
} }
} if ($bnode !== null) {
// get bnode name (try canonical, path, then hash)
// digest chosen path and update namer if ($namer->isNamed($bnode)) {
hash_update($md, $chosen_path); $name = $namer->getName($bnode);
$path_namer = $chosen_namer; } else if ($path_namer->isNamed($bnode)) {
} $name = $path_namer->getName($bnode);
} else {
// return SHA-1 hash and path namer $name = $this->_hashQuads($bnode, $bnodes, $namer);
return (object)array( }
'hash' => hash_final($md), 'pathNamer' => $path_namer);
} // hash direction, property, and bnode name/hash
$group_md = hash_init('sha256');
/** hash_update($group_md, $direction);
* A helper function that gets the blank node name from an RDF quad hash_update($group_md, $quad->predicate->value);
* node (subject or object). If the node is not a blank node or its hash_update($group_md, $name);
* value does not match the given blank node ID, it will be returned. $group_hash = hash_final($group_md);
*
* @param stdClass $node the RDF quad node. // add bnode to hash group
* @param string $id the ID of the blank node to look next to. if (property_exists($groups, $group_hash)) {
* $groups->{$group_hash}[] = $bnode;
* @return mixed the adjacent blank node name or null if none was found. } else {
*/ $groups->{$group_hash} = [$bnode];
protected function _getAdjacentBlankNodeName($node, $id) { }
if($node->type === 'blank node' && $node->value !== $id) { }
return $node->value; }
}
return null; // iterate over groups in sorted hash order
} $group_hashes = array_keys((array) $groups);
sort($group_hashes);
/** foreach ($group_hashes as $group_hash) {
* Compares two strings first based on length and then lexicographically. // digest group hash
* hash_update($md, $group_hash);
* @param string $a the first string.
* @param string $b the second string. // choose a path and namer from the permutations
* $chosen_path = null;
* @return integer -1 if a < b, 1 if a > b, 0 if a == b. $chosen_namer = null;
*/ $permutator = new Permutator($groups->{$group_hash});
protected function _compareShortestLeast($a, $b) { while ($permutator->hasNext()) {
$len_a = strlen($a); $permutation = $permutator->next();
$len_b = strlen($b); $path_namer_copy = clone $path_namer;
if($len_a < $len_b) {
return -1; // build adjacent path
} $path = '';
if($len_b < $len_a) { $skipped = false;
return 1; $recurse = [];
} foreach ($permutation as $bnode) {
if($a === $b) { // use canonical name if available
return 0; if ($namer->isNamed($bnode)) {
} $path .= $namer->getName($bnode);
return ($a < $b) ? -1 : 1; } else {
} // recurse if bnode isn't named in the path yet
if (!$path_namer_copy->isNamed($bnode)) {
/** $recurse[] = $bnode;
* Picks the preferred compaction term from the given inverse context entry. }
* $path .= $path_namer_copy->getName($bnode);
* @param active_ctx the active context. }
* @param iri the IRI to pick the term for.
* @param value the value to pick the term for. // skip permutation if path is already >= chosen path
* @param containers the preferred containers. if ($chosen_path !== null && strlen($path) >= strlen($chosen_path) &&
* @param type_or_language either '@type' or '@language'. $path > $chosen_path) {
* @param type_or_language_value the preferred value for '@type' or $skipped = true;
* '@language'. break;
* }
* @return mixed the preferred term. }
*/
protected function _selectTerm( // recurse
$active_ctx, $iri, $value, $containers, if (!$skipped) {
$type_or_language, $type_or_language_value) { foreach ($recurse as $bnode) {
if($type_or_language_value === null) { $result = $this->_hashPaths(
$type_or_language_value = '@null'; $bnode, $bnodes, $namer, $path_namer_copy);
} $path .= $path_namer_copy->getName($bnode);
$path .= "<{$result->hash}>";
// options for the value of @type or @language $path_namer_copy = $result->pathNamer;
$prefs = array();
// skip permutation if path is already >= chosen path
// determine prefs for @id based on whether or not value compacts to a term if ($chosen_path !== null &&
if(($type_or_language_value === '@id' || strlen($path) >= strlen($chosen_path) && $path > $chosen_path) {
$type_or_language_value === '@reverse') && $skipped = true;
self::_isSubjectReference($value)) { break;
// prefer @reverse first }
if($type_or_language_value === '@reverse') { }
$prefs[] = '@reverse'; }
}
// try to compact value to a term if (!$skipped && ($chosen_path === null || $path < $chosen_path)) {
$term = $this->_compactIri( $chosen_path = $path;
$active_ctx, $value->{'@id'}, null, array('vocab' => true)); $chosen_namer = $path_namer_copy;
if(property_exists($active_ctx->mappings, $term) && }
$active_ctx->mappings->{$term} && }
$active_ctx->mappings->{$term}->{'@id'} === $value->{'@id'}) {
// prefer @vocab // digest chosen path and update namer
array_push($prefs, '@vocab', '@id'); hash_update($md, $chosen_path);
} else { $path_namer = $chosen_namer;
// prefer @id }
array_push($prefs, '@id', '@vocab');
} // return SHA-1 hash and path namer
} else { return (object) [
$prefs[] = $type_or_language_value; 'hash' => hash_final($md), 'pathNamer' => $path_namer];
} }
$prefs[] = '@none';
/**
$container_map = $active_ctx->inverse->{$iri}; * A helper function that gets the blank node name from an RDF quad
foreach($containers as $container) { * node (subject or object). If the node is not a blank node or its
// if container not available in the map, continue * value does not match the given blank node ID, it will be returned.
if(!property_exists($container_map, $container)) { *
continue; * @param stdClass $node the RDF quad node.
} * @param string $id the ID of the blank node to look next to.
*
$type_or_language_value_map = * @return mixed the adjacent blank node name or null if none was found.
$container_map->{$container}->{$type_or_language}; */
foreach($prefs as $pref) { protected function _getAdjacentBlankNodeName($node, $id)
// if type/language option not available in the map, continue {
if(!property_exists($type_or_language_value_map, $pref)) { if ($node->type === 'blank node' && $node->value !== $id) {
continue; return $node->value;
} }
return null;
// select term }
return $type_or_language_value_map->{$pref};
} /**
} * Compares two strings first based on length and then lexicographically.
return null; *
} * @param string $a the first string.
* @param string $b the second string.
/** *
* Compacts an IRI or keyword into a term or prefix if it can be. If the * @return integer -1 if a < b, 1 if a > b, 0 if a == b.
* IRI has an associated value it may be passed. */
* protected function _compareShortestLeast($a, $b)
* @param stdClass $active_ctx the active context to use. {
* @param string $iri the IRI to compact. $len_a = strlen($a);
* @param mixed $value the value to check or null. $len_b = strlen($b);
* @param assoc $relative_to options for how to compact IRIs: if ($len_a < $len_b) {
* vocab: true to split after @vocab, false not to. return -1;
* @param bool $reverse true if a reverse property is being compacted, false }
* if not. if ($len_b < $len_a) {
* return 1;
* @return string the compacted term, prefix, keyword alias, or original IRI. }
*/ if ($a === $b) {
protected function _compactIri( return 0;
$active_ctx, $iri, $value=null, $relative_to=array(), $reverse=false) { }
// can't compact null return ($a < $b) ? -1 : 1;
if($iri === null) { }
return $iri;
} /**
* Picks the preferred compaction term from the given inverse context entry.
// term is a keyword, default vocab to true *
if(self::_isKeyword($iri)) { * @param active_ctx the active context.
$relative_to['vocab'] = true; * @param iri the IRI to pick the term for.
} else if(!isset($relative_to['vocab'])) { * @param value the value to pick the term for.
$relative_to['vocab'] = false; * @param containers the preferred containers.
} * @param type_or_language either '@type' or '@language'.
* @param type_or_language_value the preferred value for '@type' or
// use inverse context to pick a term if iri is relative to vocab * '@language'.
if($relative_to['vocab'] && *
property_exists($this->_getInverseContext($active_ctx), $iri)) { * @return mixed the preferred term.
$default_language = '@none'; */
if(property_exists($active_ctx, '@language')) { protected function _selectTerm(
$default_language = $active_ctx->{'@language'}; $active_ctx, $iri, $value, $containers, $type_or_language, $type_or_language_value)
} {
if ($type_or_language_value === null) {
// prefer @index if available in value $type_or_language_value = '@null';
$containers = array(); }
if(is_object($value) && property_exists($value, '@index')) {
$containers[] = '@index'; // options for the value of @type or @language
} $prefs = [];
// defaults for term selection based on type/language // determine prefs for @id based on whether or not value compacts to a term
$type_or_language = '@language'; if (($type_or_language_value === '@id' ||
$type_or_language_value = '@null'; $type_or_language_value === '@reverse') &&
self::_isSubjectReference($value)) {
if($reverse) { // prefer @reverse first
$type_or_language = '@type'; if ($type_or_language_value === '@reverse') {
$type_or_language_value = '@reverse'; $prefs[] = '@reverse';
$containers[] = '@set'; }
} else if(self::_isList($value)) { // try to compact value to a term
// choose the most specific term that works for all elements in @list $term = $this->_compactIri(
// only select @list containers if @index is NOT in value $active_ctx, $value->{'@id'}, null, ['vocab' => true]);
if(!property_exists($value, '@index')) { if (property_exists($active_ctx->mappings, $term) &&
$containers[] = '@list'; $active_ctx->mappings->{$term} &&
} $active_ctx->mappings->{$term}->{'@id'} === $value->{'@id'}) {
$list = $value->{'@list'}; // prefer @vocab
$common_language = (count($list) === 0) ? $default_language : null; array_push($prefs, '@vocab', '@id');
$common_type = null; } else {
foreach($list as $item) { // prefer @id
$item_language = '@none'; array_push($prefs, '@id', '@vocab');
$item_type = '@none'; }
if(self::_isValue($item)) { } else {
if(property_exists($item, '@language')) { $prefs[] = $type_or_language_value;
$item_language = $item->{'@language'}; }
} else if(property_exists($item, '@type')) { $prefs[] = '@none';
$item_type = $item->{'@type'};
} else { $container_map = $active_ctx->inverse->{$iri};
// plain literal foreach ($containers as $container) {
$item_language = '@null'; // if container not available in the map, continue
} if (!property_exists($container_map, $container)) {
} else { continue;
$item_type = '@id'; }
}
if($common_language === null) { $type_or_language_value_map = $container_map->{$container}->{$type_or_language};
$common_language = $item_language; foreach ($prefs as $pref) {
} else if($item_language !== $common_language && // if type/language option not available in the map, continue
self::_isValue($item)) { if (!property_exists($type_or_language_value_map, $pref)) {
$common_language = '@none'; continue;
} }
if($common_type === null) {
$common_type = $item_type; // select term
} else if($item_type !== $common_type) { return $type_or_language_value_map->{$pref};
$common_type = '@none'; }
} }
// there are different languages and types in the list, so choose return null;
// the most generic term, no need to keep iterating the list }
if($common_language === '@none' && $common_type === '@none') {
break; /**
} * Compacts an IRI or keyword into a term or prefix if it can be. If the
} * IRI has an associated value it may be passed.
if($common_language === null) { *
$common_language = '@none'; * @param stdClass $active_ctx the active context to use.
} * @param string $iri the IRI to compact.
if($common_type === null) { * @param mixed $value the value to check or null.
$common_type = '@none'; * @param assoc $relative_to options for how to compact IRIs:
} * vocab: true to split after @vocab, false not to.
if($common_type !== '@none') { * @param bool $reverse true if a reverse property is being compacted, false
$type_or_language = '@type'; * if not.
$type_or_language_value = $common_type; *
} else { * @return string the compacted term, prefix, keyword alias, or original IRI.
$type_or_language_value = $common_language; */
} protected function _compactIri(
} else { $active_ctx, $iri, $value = null, $relative_to = [], $reverse = false)
if(self::_isValue($value)) { {
if(property_exists($value, '@language') && // can't compact null
!property_exists($value, '@index')) { if ($iri === null) {
$containers[] = '@language'; return $iri;
$type_or_language_value = $value->{'@language'}; }
} else if(property_exists($value, '@type')) {
$type_or_language = '@type'; $inverse_ctx = $this->_getInverseContext($active_ctx);
$type_or_language_value = $value->{'@type'};
} if (self::_isKeyword($iri)) {
} else { // a keyword can only be compacted to simple alias
$type_or_language = '@type'; if (property_exists($inverse_ctx, $iri)) {
$type_or_language_value = '@id'; return $inverse_ctx->$iri->{'@none'}->{'@type'}->{'@none'};
} }
$containers[] = '@set'; return $iri;
} }
// do term selection if (!isset($relative_to['vocab'])) {
$containers[] = '@none'; $relative_to['vocab'] = false;
$term = $this->_selectTerm( }
$active_ctx, $iri, $value,
$containers, $type_or_language, $type_or_language_value); // use inverse context to pick a term if iri is relative to vocab
if($term !== null) { if ($relative_to['vocab'] && property_exists($inverse_ctx, $iri)) {
return $term; $default_language = '@none';
} if (property_exists($active_ctx, '@language')) {
} $default_language = $active_ctx->{'@language'};
}
// no term match, use @vocab if available
if($relative_to['vocab']) { // prefer @index if available in value
if(property_exists($active_ctx, '@vocab')) { $containers = [];
// determine if vocab is a prefix of the iri if (is_object($value) && property_exists($value, '@index')) {
$vocab = $active_ctx->{'@vocab'}; $containers[] = '@index';
if(strpos($iri, $vocab) === 0 && $iri !== $vocab) { }
// use suffix as relative iri if it is not a term in the active
// context // defaults for term selection based on type/language
$suffix = substr($iri, strlen($vocab)); $type_or_language = '@language';
if(!property_exists($active_ctx->mappings, $suffix)) { $type_or_language_value = '@null';
return $suffix;
} if ($reverse) {
} $type_or_language = '@type';
} $type_or_language_value = '@reverse';
} $containers[] = '@set';
} else if (self::_isList($value)) {
// no term or @vocab match, check for possible CURIEs // choose the most specific term that works for all elements in @list
$choice = null; // only select @list containers if @index is NOT in value
foreach($active_ctx->mappings as $term => $definition) { if (!property_exists($value, '@index')) {
// skip terms with colons, they can't be prefixes $containers[] = '@list';
if(strpos($term, ':') !== false) { }
continue; $list = $value->{'@list'};
} $common_language = (count($list) === 0) ? $default_language : null;
// skip entries with @ids that are not partial matches $common_type = null;
if($definition === null || foreach ($list as $item) {
$definition->{'@id'} === $iri || $item_language = '@none';
strpos($iri, $definition->{'@id'}) !== 0) { $item_type = '@none';
continue; if (self::_isValue($item)) {
} if (property_exists($item, '@language')) {
$item_language = $item->{'@language'};
// a CURIE is usable if: } else if (property_exists($item, '@type')) {
// 1. it has no mapping, OR $item_type = $item->{'@type'};
// 2. value is null, which means we're not compacting an @value, AND } else {
// the mapping matches the IRI) // plain literal
$curie = $term . ':' . substr($iri, strlen($definition->{'@id'})); $item_language = '@null';
$is_usable_curie = (!property_exists($active_ctx->mappings, $curie) || }
($value === null && $active_ctx->mappings->{$curie} && } else {
$active_ctx->mappings->{$curie}->{'@id'} === $iri)); $item_type = '@id';
}
// select curie if it is shorter or the same length but lexicographically if ($common_language === null) {
// less than the current choice $common_language = $item_language;
if($is_usable_curie && ($choice === null || } else if ($item_language !== $common_language &&
self::_compareShortestLeast($curie, $choice) < 0)) { self::_isValue($item)) {
$choice = $curie; $common_language = '@none';
} }
} if ($common_type === null) {
$common_type = $item_type;
// return chosen curie } else if ($item_type !== $common_type) {
if($choice !== null) { $common_type = '@none';
return $choice; }
} // there are different languages and types in the list, so choose
// the most generic term, no need to keep iterating the list
// compact IRI relative to base if ($common_language === '@none' && $common_type === '@none') {
if(!$relative_to['vocab']) { break;
return jsonld_remove_base($active_ctx->{'@base'}, $iri); }
} }
if ($common_language === null) {
// return IRI as is $common_language = '@none';
return $iri; }
} if ($common_type === null) {
$common_type = '@none';
/** }
* Performs value compaction on an object with '@value' or '@id' as the only if ($common_type !== '@none') {
* property. $type_or_language = '@type';
* $type_or_language_value = $common_type;
* @param stdClass $active_ctx the active context. } else {
* @param string $active_property the active property that points to the $type_or_language_value = $common_language;
* value. }
* @param mixed $value the value to compact. } else {
* if (self::_isValue($value)) {
* @return mixed the compaction result. if (property_exists($value, '@language') &&
*/ !property_exists($value, '@index')) {
protected function _compactValue($active_ctx, $active_property, $value) { $containers[] = '@language';
// value is a @value $type_or_language_value = $value->{'@language'};
if(self::_isValue($value)) { } else if (property_exists($value, '@type')) {
// get context rules $type_or_language = '@type';
$type = self::getContextValue($active_ctx, $active_property, '@type'); $type_or_language_value = $value->{'@type'};
$language = self::getContextValue( }
$active_ctx, $active_property, '@language'); } else {
$container = self::getContextValue( $type_or_language = '@type';
$active_ctx, $active_property, '@container'); $type_or_language_value = '@id';
}
// whether or not the value has an @index that must be preserved $containers[] = '@set';
$preserve_index = (property_exists($value, '@index') && }
$container !== '@index');
// do term selection
// if there's no @index to preserve $containers[] = '@none';
if(!$preserve_index) { $term = $this->_selectTerm(
// matching @type or @language specified in context, compact value $active_ctx, $iri, $value, $containers, $type_or_language, $type_or_language_value);
if(self::_hasKeyValue($value, '@type', $type) || if ($term !== null) {
self::_hasKeyValue($value, '@language', $language)) { return $term;
return $value->{'@value'}; }
} }
}
// no term match, use @vocab if available
// return just the value of @value if all are true: if ($relative_to['vocab']) {
// 1. @value is the only key or @index isn't being preserved if (property_exists($active_ctx, '@vocab')) {
// 2. there is no default language or @value is not a string or // determine if vocab is a prefix of the iri
// the key has a mapping with a null @language $vocab = $active_ctx->{'@vocab'};
$key_count = count(array_keys((array)$value)); if (strpos($iri, $vocab) === 0 && $iri !== $vocab) {
$is_value_only_key = ($key_count === 1 || // use suffix as relative iri if it is not a term in the active
($key_count === 2 && property_exists($value, '@index') && // context
!$preserve_index)); $suffix = substr($iri, strlen($vocab));
$has_default_language = property_exists($active_ctx, '@language'); if (!property_exists($active_ctx->mappings, $suffix)) {
$is_value_string = is_string($value->{'@value'}); return $suffix;
$has_null_mapping = ( }
property_exists($active_ctx->mappings, $active_property) && }
$active_ctx->mappings->{$active_property} !== null && }
self::_hasKeyValue( }
$active_ctx->mappings->{$active_property}, '@language', null));
if($is_value_only_key && // no term or @vocab match, check for possible CURIEs
(!$has_default_language || !$is_value_string || $has_null_mapping)) { $choice = null;
return $value->{'@value'}; $idx = 0;
} $partial_matches = [];
$iri_map = $active_ctx->fast_curie_map;
$rval = new stdClass(); // check for partial matches of against `iri`, which means look until
// iri.length - 1, not full length
// preserve @index $max_partial_length = strlen($iri) - 1;
if($preserve_index) { for (; $idx < $max_partial_length && isset($iri_map[$iri[$idx]]); ++$idx) {
$rval->{$this->_compactIri($active_ctx, '@index')} = $value->{'@index'}; $iri_map = $iri_map[$iri[$idx]];
} if (isset($iri_map[''])) {
$entry = $iri_map[''][0];
// compact @type IRI $entry->iri_length = $idx + 1;
if(property_exists($value, '@type')) { $partial_matches[] = $entry;
$rval->{$this->_compactIri($active_ctx, '@type')} = $this->_compactIri( }
$active_ctx, $value->{'@type'}, null, array('vocab' => true)); }
} else if(property_exists($value, '@language')) { // check partial matches in reverse order to prefer longest ones first
// alias @language $partial_matches = array_reverse($partial_matches);
$rval->{$this->_compactIri($active_ctx, '@language')} = foreach ($partial_matches as $entry) {
$value->{'@language'}; $terms = $entry->terms;
} foreach ($terms as $term) {
// a CURIE is usable if:
// alias @value // 1. it has no mapping, OR
$rval->{$this->_compactIri($active_ctx, '@value')} = $value->{'@value'}; // 2. value is null, which means we're not compacting an @value, AND
// the mapping matches the IRI
return $rval; $curie = $term . ':' . substr($iri, $entry->iri_length);
} $is_usable_curie = (!property_exists($active_ctx->mappings, $curie) ||
($value === null &&
// value is a subject reference $active_ctx->mappings->{$curie}->{'@id'} === $iri));
$expanded_property = $this->_expandIri(
$active_ctx, $active_property, array('vocab' => true)); // select curie if it is shorter or the same length but
$type = self::getContextValue($active_ctx, $active_property, '@type'); // lexicographically less than the current choice
$compacted = $this->_compactIri( if ($is_usable_curie && ($choice === null ||
$active_ctx, $value->{'@id'}, null, self::_compareShortestLeast($curie, $choice) < 0)) {
array('vocab' => ($type === '@vocab'))); $choice = $curie;
}
// compact to scalar }
if($type === '@id' || $type === '@vocab' || }
$expanded_property === '@graph') {
return $compacted; // return chosen curie
} if ($choice !== null) {
return $choice;
$rval = (object)array( }
$this->_compactIri($active_ctx, '@id') => $compacted);
return $rval; // compact IRI relative to base
} if (!$relative_to['vocab']) {
return jsonld_remove_base($active_ctx->{'@base'}, $iri);
/** }
* Creates a term definition during context processing.
* // return IRI as is
* @param stdClass $active_ctx the current active context. return $iri;
* @param stdClass $local_ctx the local context being processed. }
* @param string $term the key in the local context to define the mapping for.
* @param stdClass $defined a map of defining/defined keys to detect cycles /**
* and prevent double definitions. * Performs value compaction on an object with '@value' or '@id' as the only
*/ * property.
protected function _createTermDefinition( *
$active_ctx, $local_ctx, $term, $defined) { * @param stdClass $active_ctx the active context.
if(property_exists($defined, $term)) { * @param string $active_property the active property that points to the
// term already defined * value.
if($defined->{$term}) { * @param mixed $value the value to compact.
return; *
} * @return mixed the compaction result.
// cycle detected */
throw new JsonLdException( protected function _compactValue($active_ctx, $active_property, $value)
'Cyclical context definition detected.', {
'jsonld.CyclicalContext', 'cyclic IRI mapping', // value is a @value
array('context' => $local_ctx, 'term' => $term)); if (self::_isValue($value)) {
} // get context rules
$type = self::getContextValue($active_ctx, $active_property, '@type');
// now defining term $language = self::getContextValue(
$defined->{$term} = false; $active_ctx, $active_property, '@language');
$container = self::getContextValue(
if(self::_isKeyword($term)) { $active_ctx, $active_property, '@container');
throw new JsonLdException(
'Invalid JSON-LD syntax; keywords cannot be overridden.', // whether or not the value has an @index that must be preserved
'jsonld.SyntaxError', 'keyword redefinition', $preserve_index = (property_exists($value, '@index') &&
array('context' => $local_ctx, 'term' => $term)); $container !== '@index');
}
// if there's no @index to preserve
// remove old mapping if (!$preserve_index) {
if(property_exists($active_ctx->mappings, $term)) { // matching @type or @language specified in context, compact value
unset($active_ctx->mappings->{$term}); if (self::_hasKeyValue($value, '@type', $type) ||
} self::_hasKeyValue($value, '@language', $language)) {
return $value->{'@value'};
// get context term value }
$value = $local_ctx->{$term}; }
// clear context entry // return just the value of @value if all are true:
if($value === null || (is_object($value) && // 1. @value is the only key or @index isn't being preserved
self::_hasKeyValue($value, '@id', null))) { // 2. there is no default language or @value is not a string or
$active_ctx->mappings->{$term} = null; // the key has a mapping with a null @language
$defined->{$term} = true; $key_count = count(array_keys((array) $value));
return; $is_value_only_key = ($key_count === 1 ||
} ($key_count === 2 && property_exists($value, '@index') &&
!$preserve_index));
// convert short-hand value to object w/@id $has_default_language = property_exists($active_ctx, '@language');
if(is_string($value)) { $is_value_string = is_string($value->{'@value'});
$value = (object)array('@id' => $value); $has_null_mapping = (
} property_exists($active_ctx->mappings, $active_property) &&
$active_ctx->mappings->{$active_property} !== null &&
if(!is_object($value)) { self::_hasKeyValue(
throw new JsonLdException( $active_ctx->mappings->{$active_property}, '@language', null));
'Invalid JSON-LD syntax; @context property values must be ' . if ($is_value_only_key &&
'strings or objects.', 'jsonld.SyntaxError', 'invalid term definition', (!$has_default_language || !$is_value_string || $has_null_mapping)) {
array('context' => $local_ctx)); return $value->{'@value'};
} }
// create new mapping $rval = new stdClass();
$mapping = $active_ctx->mappings->{$term} = new stdClass();
$mapping->reverse = false; // preserve @index
if ($preserve_index) {
if(property_exists($value, '@reverse')) { $rval->{$this->_compactIri($active_ctx, '@index')} = $value->{'@index'};
if(property_exists($value, '@id')) { }
throw new JsonLdException(
'Invalid JSON-LD syntax; a @reverse term definition must not ' + // compact @type IRI
'contain @id.', 'jsonld.SyntaxError', 'invalid reverse property', if (property_exists($value, '@type')) {
array('context' => $local_ctx)); $rval->{$this->_compactIri($active_ctx, '@type')} = $this->_compactIri(
} $active_ctx, $value->{'@type'}, null, ['vocab' => true]);
$reverse = $value->{'@reverse'}; } else if (property_exists($value, '@language')) {
if(!is_string($reverse)) { // alias @language
throw new JsonLdException( $rval->{$this->_compactIri($active_ctx, '@language')} = $value->{'@language'};
'Invalid JSON-LD syntax; a @context @reverse value must be a string.', }
'jsonld.SyntaxError', 'invalid IRI mapping',
array('context' => $local_ctx)); // alias @value
} $rval->{$this->_compactIri($active_ctx, '@value')} = $value->{'@value'};
// expand and add @id mapping return $rval;
$id = $this->_expandIri( }
$active_ctx, $reverse, array('vocab' => true, 'base' => false),
$local_ctx, $defined); // value is a subject reference
if(!self::_isAbsoluteIri($id)) { $expanded_property = $this->_expandIri(
throw new JsonLdException( $active_ctx, $active_property, ['vocab' => true]);
'Invalid JSON-LD syntax; @context @reverse value must be ' . $type = self::getContextValue($active_ctx, $active_property, '@type');
'an absolute IRI or a blank node identifier.', $compacted = $this->_compactIri(
'jsonld.SyntaxError', 'invalid IRI mapping', $active_ctx, $value->{'@id'}, null, ['vocab' => ($type === '@vocab')]);
array('context' => $local_ctx));
} // compact to scalar
$mapping->{'@id'} = $id; if ($type === '@id' || $type === '@vocab' ||
$mapping->reverse = true; $expanded_property === '@graph') {
} else if(property_exists($value, '@id')) { return $compacted;
$id = $value->{'@id'}; }
if(!is_string($id)) {
throw new JsonLdException( $rval = (object) [
'Invalid JSON-LD syntax; @context @id value must be a string.', $this->_compactIri($active_ctx, '@id') => $compacted];
'jsonld.SyntaxError', 'invalid IRI mapping', return $rval;
array('context' => $local_ctx)); }
}
if($id !== $term) { /**
// add @id to mapping * Creates a term definition during context processing.
$id = $this->_expandIri( *
$active_ctx, $id, array('vocab' => true, 'base' => false), * @param stdClass $active_ctx the current active context.
$local_ctx, $defined); * @param stdClass $local_ctx the local context being processed.
if(!self::_isAbsoluteIri($id) && !self::_isKeyword($id)) { * @param string $term the key in the local context to define the mapping for.
throw new JsonLdException( * @param stdClass $defined a map of defining/defined keys to detect cycles
'Invalid JSON-LD syntax; @context @id value must be an ' . * and prevent double definitions.
'absolute IRI, a blank node identifier, or a keyword.', */
'jsonld.SyntaxError', 'invalid IRI mapping', protected function _createTermDefinition(
array('context' => $local_ctx)); $active_ctx, $local_ctx, $term, $defined)
} {
$mapping->{'@id'} = $id; if (property_exists($defined, $term)) {
} // term already defined
} if ($defined->{$term}) {
return;
if(!property_exists($mapping, '@id')) { }
// see if the term has a prefix // cycle detected
$colon = strpos($term, ':'); throw new JsonLdException(
if($colon !== false) { 'Cyclical context definition detected.', 'jsonld.CyclicalContext', 'cyclic IRI mapping', ['context' => $local_ctx, 'term' => $term]);
$prefix = substr($term, 0, $colon); }
if(property_exists($local_ctx, $prefix)) {
// define parent prefix // now defining term
$this->_createTermDefinition( $defined->{$term} = false;
$active_ctx, $local_ctx, $prefix, $defined);
} if (self::_isKeyword($term)) {
throw new JsonLdException(
if(property_exists($active_ctx->mappings, $prefix) && 'Invalid JSON-LD syntax; keywords cannot be overridden.', 'jsonld.SyntaxError', 'keyword redefinition', ['context' => $local_ctx, 'term' => $term]);
$active_ctx->mappings->{$prefix}) { }
// set @id based on prefix parent
$suffix = substr($term, $colon + 1); // remove old mapping
$mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} . if (property_exists($active_ctx->mappings, $term)) {
$suffix; unset($active_ctx->mappings->{$term});
} else { }
// term is an absolute IRI
$mapping->{'@id'} = $term; // get context term value
} $value = $local_ctx->{$term};
} else {
// non-IRIs *must* define @ids if @vocab is not available // clear context entry
if(!property_exists($active_ctx, '@vocab')) { if ($value === null || (is_object($value) &&
throw new JsonLdException( self::_hasKeyValue($value, '@id', null))) {
'Invalid JSON-LD syntax; @context terms must define an @id.', $active_ctx->mappings->{$term} = null;
'jsonld.SyntaxError', 'invalid IRI mapping', $defined->{$term} = true;
array('context' => $local_ctx, 'term' => $term)); return;
} }
// prepend vocab to term
$mapping->{'@id'} = $active_ctx->{'@vocab'} . $term; // convert short-hand value to object w/@id
} if (is_string($value)) {
} $value = (object) ['@id' => $value];
}
// IRI mapping now defined
$defined->{$term} = true; if (!is_object($value)) {
throw new JsonLdException(
if(property_exists($value, '@type')) { 'Invalid JSON-LD syntax; @context property values must be ' .
$type = $value->{'@type'}; 'strings or objects.', 'jsonld.SyntaxError', 'invalid term definition', ['context' => $local_ctx]);
if(!is_string($type)) { }
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @type values must be strings.', // create new mapping
'jsonld.SyntaxError', 'invalid type mapping', $mapping = $active_ctx->mappings->{$term} = new stdClass();
array('context' => $local_ctx)); $mapping->reverse = false;
}
if (property_exists($value, '@reverse')) {
if($type !== '@id' && $type !== '@vocab') { if (property_exists($value, '@id')) {
// expand @type to full IRI throw new JsonLdException(
$type = $this->_expandIri( 'Invalid JSON-LD syntax; a @reverse term definition must not ' +
$active_ctx, $type, array('vocab' => true), $local_ctx, $defined); 'contain @id.', 'jsonld.SyntaxError', 'invalid reverse property', ['context' => $local_ctx]);
if(!self::_isAbsoluteIri($type)) { }
throw new JsonLdException( $reverse = $value->{'@reverse'};
'Invalid JSON-LD syntax; an @context @type value must ' . if (!is_string($reverse)) {
'be an absolute IRI.', 'jsonld.SyntaxError', throw new JsonLdException(
'invalid type mapping', array('context' => $local_ctx)); 'Invalid JSON-LD syntax; a @context @reverse value must be a string.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
} }
if(strpos($type, '_:') === 0) {
throw new JsonLdException( // expand and add @id mapping
'Invalid JSON-LD syntax; an @context @type values must ' . $id = $this->_expandIri(
'be an IRI, not a blank node identifier.', $active_ctx, $reverse, ['vocab' => true, 'base' => false], $local_ctx, $defined);
'jsonld.SyntaxError', 'invalid type mapping', if (!self::_isAbsoluteIri($id)) {
array('context' => $local_ctx)); throw new JsonLdException(
} 'Invalid JSON-LD syntax; @context @reverse value must be ' .
} 'an absolute IRI or a blank node identifier.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
}
// add @type to mapping $mapping->{'@id'} = $id;
$mapping->{'@type'} = $type; $mapping->reverse = true;
} } else if (property_exists($value, '@id')) {
$id = $value->{'@id'};
if(property_exists($value, '@container')) { if (!is_string($id)) {
$container = $value->{'@container'}; throw new JsonLdException(
if($container !== '@list' && $container !== '@set' && 'Invalid JSON-LD syntax; @context @id value must be a string.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
$container !== '@index' && $container !== '@language') { }
throw new JsonLdException( if ($id !== $term) {
'Invalid JSON-LD syntax; @context @container value must be ' . // add @id to mapping
'one of the following: @list, @set, @index, or @language.', $id = $this->_expandIri(
'jsonld.SyntaxError', 'invalid container mapping', $active_ctx, $id, ['vocab' => true, 'base' => false], $local_ctx, $defined);
array('context' => $local_ctx)); if (!self::_isAbsoluteIri($id) && !self::_isKeyword($id)) {
} throw new JsonLdException(
if($mapping->reverse && $container !== '@index' && 'Invalid JSON-LD syntax; @context @id value must be an ' .
$container !== '@set' && $container !== null) { 'absolute IRI, a blank node identifier, or a keyword.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
throw new JsonLdException( }
'Invalid JSON-LD syntax; @context @container value for a @reverse ' + $mapping->{'@id'} = $id;
'type definition must be @index or @set.', }
'jsonld.SyntaxError', 'invalid reverse property', }
array('context' => $local_ctx));
} // always compute whether term has a colon as an optimization for
// _compactIri
// add @container to mapping $colon = strpos($term, ':');
$mapping->{'@container'} = $container; $mapping->_term_has_colon = ($colon !== false);
}
if (!property_exists($mapping, '@id')) {
if(property_exists($value, '@language') && // see if the term has a prefix
!property_exists($value, '@type')) { if ($mapping->_term_has_colon) {
$language = $value->{'@language'}; $prefix = substr($term, 0, $colon);
if($language !== null && !is_string($language)) { if (property_exists($local_ctx, $prefix)) {
throw new JsonLdException( // define parent prefix
'Invalid JSON-LD syntax; @context @language value must be ' . $this->_createTermDefinition(
'a string or null.', 'jsonld.SyntaxError', $active_ctx, $local_ctx, $prefix, $defined);
'invalid language mapping', array('context' => $local_ctx)); }
}
if (property_exists($active_ctx->mappings, $prefix) &&
// add @language to mapping $active_ctx->mappings->{$prefix}) {
if($language !== null) { // set @id based on prefix parent
$language = strtolower($language); $suffix = substr($term, $colon + 1);
} $mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} .
$mapping->{'@language'} = $language; $suffix;
} } else {
// term is an absolute IRI
// disallow aliasing @context and @preserve $mapping->{'@id'} = $term;
$id = $mapping->{'@id'}; }
if($id === '@context' || $id === '@preserve') { } else {
throw new JsonLdException( // non-IRIs *must* define @ids if @vocab is not available
'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.', if (!property_exists($active_ctx, '@vocab')) {
'jsonld.SyntaxError', 'invalid keyword alias', throw new JsonLdException(
array('context' => $local_ctx)); 'Invalid JSON-LD syntax; @context terms must define an @id.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx, 'term' => $term]);
} }
} // prepend vocab to term
$mapping->{'@id'} = $active_ctx->{'@vocab'} . $term;
/** }
* Expands a string to a full IRI. The string may be a term, a prefix, a }
* relative IRI, or an absolute IRI. The associated absolute IRI will be
* returned. // optimization to store length of @id once for _compactIri
* $mapping->_id_length = strlen($mapping->{'@id'});
* @param stdClass $active_ctx the current active context.
* @param string $value the string to expand. // IRI mapping now defined
* @param assoc $relative_to options for how to resolve relative IRIs: $defined->{$term} = true;
* base: true to resolve against the base IRI, false not to.
* vocab: true to concatenate after @vocab, false not to. if (property_exists($value, '@type')) {
* @param stdClass $local_ctx the local context being processed (only given $type = $value->{'@type'};
* if called during document processing). if (!is_string($type)) {
* @param defined a map for tracking cycles in context definitions (only given throw new JsonLdException(
* if called during document processing). 'Invalid JSON-LD syntax; @context @type values must be strings.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]);
* }
* @return mixed the expanded value.
*/ if ($type !== '@id' && $type !== '@vocab') {
function _expandIri( // expand @type to full IRI
$active_ctx, $value, $relative_to=array(), $local_ctx=null, $defined=null) { $type = $this->_expandIri(
// already expanded $active_ctx, $type, ['vocab' => true], $local_ctx, $defined);
if($value === null || self::_isKeyword($value)) { if (!self::_isAbsoluteIri($type)) {
return $value; throw new JsonLdException(
} 'Invalid JSON-LD syntax; an @context @type value must ' .
'be an absolute IRI.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]);
// define term dependency if not defined }
if($local_ctx !== null && property_exists($local_ctx, $value) && if (strpos($type, '_:') === 0) {
!self::_hasKeyValue($defined, $value, true)) { throw new JsonLdException(
$this->_createTermDefinition($active_ctx, $local_ctx, $value, $defined); 'Invalid JSON-LD syntax; an @context @type values must ' .
} 'be an IRI, not a blank node identifier.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]);
}
if(isset($relative_to['vocab']) && $relative_to['vocab']) { }
if(property_exists($active_ctx->mappings, $value)) {
$mapping = $active_ctx->mappings->{$value}; // add @type to mapping
$mapping->{'@type'} = $type;
// value is explicitly ignored with a null mapping }
if($mapping === null) {
return null; if (property_exists($value, '@container')) {
} $container = $value->{'@container'};
if ($container !== '@list' && $container !== '@set' &&
// value is a term $container !== '@index' && $container !== '@language') {
return $mapping->{'@id'}; throw new JsonLdException(
} 'Invalid JSON-LD syntax; @context @container value must be ' .
} 'one of the following: @list, @set, @index, or @language.', 'jsonld.SyntaxError', 'invalid container mapping', ['context' => $local_ctx]);
}
// split value into prefix:suffix if ($mapping->reverse && $container !== '@index' &&
$colon = strpos($value, ':'); $container !== '@set' && $container !== null) {
if($colon !== false) { throw new JsonLdException(
$prefix = substr($value, 0, $colon); 'Invalid JSON-LD syntax; @context @container value for a @reverse ' +
$suffix = substr($value, $colon + 1); 'type definition must be @index or @set.', 'jsonld.SyntaxError', 'invalid reverse property', ['context' => $local_ctx]);
}
// do not expand blank nodes (prefix of '_') or already-absolute
// IRIs (suffix of '//') // add @container to mapping
if($prefix === '_' || strpos($suffix, '//') === 0) { $mapping->{'@container'} = $container;
return $value; }
}
if (property_exists($value, '@language') &&
// prefix dependency not defined, define it !property_exists($value, '@type')) {
if($local_ctx !== null && property_exists($local_ctx, $prefix)) { $language = $value->{'@language'};
$this->_createTermDefinition( if ($language !== null && !is_string($language)) {
$active_ctx, $local_ctx, $prefix, $defined); throw new JsonLdException(
} 'Invalid JSON-LD syntax; @context @language value must be ' .
'a string or null.', 'jsonld.SyntaxError', 'invalid language mapping', ['context' => $local_ctx]);
// use mapping if prefix is defined }
if(property_exists($active_ctx->mappings, $prefix)) {
$mapping = $active_ctx->mappings->{$prefix}; // add @language to mapping
if($mapping) { if ($language !== null) {
return $mapping->{'@id'} . $suffix; $language = strtolower($language);
} }
} $mapping->{'@language'} = $language;
}
// already absolute IRI
return $value; // disallow aliasing @context and @preserve
} $id = $mapping->{'@id'};
if ($id === '@context' || $id === '@preserve') {
// prepend vocab throw new JsonLdException(
if(isset($relative_to['vocab']) && $relative_to['vocab'] && 'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.', 'jsonld.SyntaxError', 'invalid keyword alias', ['context' => $local_ctx]);
property_exists($active_ctx, '@vocab')) { }
return $active_ctx->{'@vocab'} . $value; }
}
/**
// prepend base * Expands a string to a full IRI. The string may be a term, a prefix, a
$rval = $value; * relative IRI, or an absolute IRI. The associated absolute IRI will be
if(isset($relative_to['base']) && $relative_to['base']) { * returned.
$rval = jsonld_prepend_base($active_ctx->{'@base'}, $rval); *
} * @param stdClass $active_ctx the current active context.
* @param string $value the string to expand.
return $rval; * @param assoc $relative_to options for how to resolve relative IRIs:
} * base: true to resolve against the base IRI, false not to.
* vocab: true to concatenate after @vocab, false not to.
/** * @param stdClass $local_ctx the local context being processed (only given
* Finds all @context URLs in the given JSON-LD input. * if called during document processing).
* * @param defined a map for tracking cycles in context definitions (only given
* @param mixed $input the JSON-LD input. * if called during document processing).
* @param stdClass $urls a map of URLs (url => false/@contexts). *
* @param bool $replace true to replace the URLs in the given input with * @return mixed the expanded value.
* the @contexts from the urls map, false not to. */
* @param string $base the base URL to resolve relative URLs with. function _expandIri(
*/ $active_ctx, $value, $relative_to = [], $local_ctx = null, $defined = null)
protected function _findContextUrls($input, $urls, $replace, $base) { {
if(is_array($input)) { // already expanded
foreach($input as $e) { if ($value === null || self::_isKeyword($value)) {
$this->_findContextUrls($e, $urls, $replace, $base); return $value;
} }
} else if(is_object($input)) {
foreach($input as $k => &$v) { // define term dependency if not defined
if($k !== '@context') { if ($local_ctx !== null && property_exists($local_ctx, $value) &&
$this->_findContextUrls($v, $urls, $replace, $base); !self::_hasKeyValue($defined, $value, true)) {
continue; $this->_createTermDefinition($active_ctx, $local_ctx, $value, $defined);
} }
// array @context if (isset($relative_to['vocab']) && $relative_to['vocab']) {
if(is_array($v)) { if (property_exists($active_ctx->mappings, $value)) {
$length = count($v); $mapping = $active_ctx->mappings->{$value};
for($i = 0; $i < $length; ++$i) {
if(is_string($v[$i])) { // value is explicitly ignored with a null mapping
$url = jsonld_prepend_base($base, $v[$i]); if ($mapping === null) {
// replace w/@context if requested return null;
if($replace) { }
$ctx = $urls->{$url};
if(is_array($ctx)) { // value is a term
// add flattened context return $mapping->{'@id'};
array_splice($v, $i, 1, $ctx); }
$i += count($ctx); }
$length += count($ctx);
} else { // split value into prefix:suffix
$v[$i] = $ctx; $colon = strpos($value, ':');
} if ($colon !== false) {
} else if(!property_exists($urls, $url)) { $prefix = substr($value, 0, $colon);
// @context URL found $suffix = substr($value, $colon + 1);
$urls->{$url} = false;
} // do not expand blank nodes (prefix of '_') or already-absolute
} // IRIs (suffix of '//')
} if ($prefix === '_' || strpos($suffix, '//') === 0) {
} else if(is_string($v)) { return $value;
// string @context }
$v = jsonld_prepend_base($base, $v);
// replace w/@context if requested // prefix dependency not defined, define it
if($replace) { if ($local_ctx !== null && property_exists($local_ctx, $prefix)) {
$input->{$k} = $urls->{$v}; $this->_createTermDefinition(
} else if(!property_exists($urls, $v)) { $active_ctx, $local_ctx, $prefix, $defined);
// @context URL found }
$urls->{$v} = false;
} // use mapping if prefix is defined
} if (property_exists($active_ctx->mappings, $prefix)) {
} $mapping = $active_ctx->mappings->{$prefix};
} if ($mapping) {
} return $mapping->{'@id'} . $suffix;
}
/** }
* Retrieves external @context URLs using the given document loader. Each
* instance of @context in the input that refers to a URL will be replaced // already absolute IRI
* with the JSON @context found at that URL. return $value;
* }
* @param mixed $input the JSON-LD input with possible contexts.
* @param stdClass $cycles an object for tracking context cycles. // prepend vocab
* @param callable $load_document(url) the document loader. if (isset($relative_to['vocab']) && $relative_to['vocab'] &&
* @param base $base the base URL to resolve relative URLs against. property_exists($active_ctx, '@vocab')) {
* return $active_ctx->{'@vocab'} . $value;
* @return mixed the result. }
*/
protected function _retrieveContextUrls( // prepend base
&$input, $cycles, $load_document, $base='') { $rval = $value;
if(count(get_object_vars($cycles)) > self::MAX_CONTEXT_URLS) { if (isset($relative_to['base']) && $relative_to['base']) {
throw new JsonLdException( $rval = jsonld_prepend_base($active_ctx->{'@base'}, $rval);
'Maximum number of @context URLs exceeded.', }
'jsonld.ContextUrlError', 'loading remote context failed',
array('max' => self::MAX_CONTEXT_URLS)); return $rval;
} }
// for tracking the URLs to retrieve /**
$urls = new stdClass(); * Finds all @context URLs in the given JSON-LD input.
*
// regex for validating URLs * @param mixed $input the JSON-LD input.
$regex = '/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/'; * @param stdClass $urls a map of URLs (url => false/@contexts).
* @param bool $replace true to replace the URLs in the given input with
// find all URLs in the given input * the @contexts from the urls map, false not to.
$this->_findContextUrls($input, $urls, false, $base); * @param string $base the base URL to resolve relative URLs with.
*/
// queue all unretrieved URLs protected function _findContextUrls($input, $urls, $replace, $base)
$queue = array(); {
foreach($urls as $url => $ctx) { if (is_array($input)) {
if($ctx === false) { foreach ($input as $e) {
// validate URL $this->_findContextUrls($e, $urls, $replace, $base);
if(!preg_match($regex, $url)) { }
throw new JsonLdException( } else if (is_object($input)) {
'Malformed or unsupported URL.', 'jsonld.InvalidUrl', foreach ($input as $k => &$v) {
'loading remote context failed', array('url' => $url)); if ($k !== '@context') {
} $this->_findContextUrls($v, $urls, $replace, $base);
$queue[] = $url; continue;
} }
}
// array @context
// retrieve URLs in queue if (is_array($v)) {
foreach($queue as $url) { $length = count($v);
// check for context URL cycle for ($i = 0; $i < $length; ++$i) {
if(property_exists($cycles, $url)) { if (is_string($v[$i])) {
throw new JsonLdException( $url = jsonld_prepend_base($base, $v[$i]);
'Cyclical @context URLs detected.', // replace w/@context if requested
'jsonld.ContextUrlError', 'recursive context inclusion', if ($replace) {
array('url' => $url)); $ctx = $urls->{$url};
} if (is_array($ctx)) {
$_cycles = self::copy($cycles); // add flattened context
$_cycles->{$url} = true; array_splice($v, $i, 1, $ctx);
$i += count($ctx) - 1;
// retrieve URL $length = count($v);
$remote_doc = call_user_func($load_document, $url); } else {
$ctx = $remote_doc->document; $v[$i] = $ctx;
}
// parse string context as JSON } else if (!property_exists($urls, $url)) {
if(is_string($ctx)) { // @context URL found
try { $urls->{$url} = false;
$ctx = self::_parse_json($ctx); }
} catch(Exception $e) { }
throw new JsonLdException( }
'Could not parse JSON from URL.', } else if (is_string($v)) {
'jsonld.ParseError', 'loading remote context failed', // string @context
array('url' => $url), $e); $v = jsonld_prepend_base($base, $v);
} // replace w/@context if requested
} if ($replace) {
$input->{$k} = $urls->{$v};
// ensure ctx is an object } else if (!property_exists($urls, $v)) {
if(!is_object($ctx)) { // @context URL found
throw new JsonLdException( $urls->{$v} = false;
'Derefencing a URL did not result in a valid JSON-LD object.', }
'jsonld.InvalidUrl', 'invalid remote context', array('url' => $url)); }
} }
}
// use empty context if no @context key is present }
if(!property_exists($ctx, '@context')) {
$ctx = (object)array('@context' => new stdClass()); /**
} else { * Retrieves external @context URLs using the given document loader. Each
$ctx = (object)array('@context' => $ctx->{'@context'}); * instance of @context in the input that refers to a URL will be replaced
} * with the JSON @context found at that URL.
*
// append context URL to context if given * @param mixed $input the JSON-LD input with possible contexts.
if($remote_doc->contextUrl !== null) { * @param stdClass $cycles an object for tracking context cycles.
$ctx->{'@context'} = self::arrayify($ctx->{'@context'}); * @param callable $load_document(url) the document loader.
$ctx->{'@context'}[] = $remote_doc->contextUrl; * @param base $base the base URL to resolve relative URLs against.
} *
* @return mixed the result.
// recurse */
$this->_retrieveContextUrls($ctx, $_cycles, $load_document, $url); protected function _retrieveContextUrls(
$urls->{$url} = $ctx->{'@context'}; &$input, $cycles, $load_document, $base = '')
} {
if (count(get_object_vars($cycles)) > self::MAX_CONTEXT_URLS) {
// replace all URLS in the input throw new JsonLdException(
$this->_findContextUrls($input, $urls, true, $base); 'Maximum number of @context URLs exceeded.', 'jsonld.ContextUrlError', 'loading remote context failed', ['max' => self::MAX_CONTEXT_URLS]);
} }
/** // for tracking the URLs to retrieve
* Gets the initial context. $urls = new stdClass();
*
* @param assoc $options the options to use. // regex for validating URLs
* base the document base IRI. $regex = '/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/';
*
* @return stdClass the initial context. // find all URLs in the given input
*/ $this->_findContextUrls($input, $urls, false, $base);
protected function _getInitialContext($options) {
return (object)array( // queue all unretrieved URLs
'@base' => jsonld_parse_url($options['base']), $queue = [];
'mappings' => new stdClass(), foreach ($urls as $url => $ctx) {
'inverse' => null); if ($ctx === false) {
} // validate URL
if (!preg_match($regex, $url)) {
/** throw new JsonLdException(
* Generates an inverse context for use in the compaction algorithm, if 'Malformed or unsupported URL.', 'jsonld.InvalidUrl', 'loading remote context failed', ['url' => $url]);
* not already generated for the given active context. }
* $queue[] = $url;
* @param stdClass $active_ctx the active context to use. }
* }
* @return stdClass the inverse context.
*/ // retrieve URLs in queue
protected function _getInverseContext($active_ctx) { foreach ($queue as $url) {
// inverse context already generated // check for context URL cycle
if($active_ctx->inverse) { if (property_exists($cycles, $url)) {
return $active_ctx->inverse; throw new JsonLdException(
} 'Cyclical @context URLs detected.', 'jsonld.ContextUrlError', 'recursive context inclusion', ['url' => $url]);
}
$inverse = $active_ctx->inverse = new stdClass(); $_cycles = self::copy($cycles);
$_cycles->{$url} = true;
// handle default language
$default_language = '@none'; // retrieve URL
if(property_exists($active_ctx, '@language')) { $remote_doc = call_user_func($load_document, $url);
$default_language = $active_ctx->{'@language'}; $ctx = $remote_doc->document;
}
// parse string context as JSON
// create term selections for each mapping in the context, ordered by if (is_string($ctx)) {
// shortest and then lexicographically least try {
$mappings = $active_ctx->mappings; $ctx = self::_parse_json($ctx);
$terms = array_keys((array)$mappings); } catch (Exception $e) {
usort($terms, array($this, '_compareShortestLeast')); throw new JsonLdException(
foreach($terms as $term) { 'Could not parse JSON from URL.', 'jsonld.ParseError', 'loading remote context failed', ['url' => $url], $e);
$mapping = $mappings->{$term}; }
if($mapping === null) { }
continue;
} // ensure ctx is an object
if (!is_object($ctx)) {
// add term selection where it applies throw new JsonLdException(
if(property_exists($mapping, '@container')) { 'Derefencing a URL did not result in a valid JSON-LD object.', 'jsonld.InvalidUrl', 'invalid remote context', ['url' => $url]);
$container = $mapping->{'@container'}; }
} else {
$container = '@none'; // use empty context if no @context key is present
} if (!property_exists($ctx, '@context')) {
$ctx = (object) ['@context' => new stdClass()];
// iterate over every IRI in the mapping } else {
$iris = $mapping->{'@id'}; $ctx = (object) ['@context' => $ctx->{'@context'}];
$iris = self::arrayify($iris); }
foreach($iris as $iri) {
// initialize container map // append context URL to context if given
if(!property_exists($inverse, $iri)) { if ($remote_doc->contextUrl !== null) {
$inverse->{$iri} = new stdClass(); $ctx->{'@context'} = self::arrayify($ctx->{'@context'});
} $ctx->{'@context'}[] = $remote_doc->contextUrl;
$container_map = $inverse->{$iri}; }
// add new entry // recurse
if(!property_exists($container_map, $container)) { $this->_retrieveContextUrls($ctx, $_cycles, $load_document, $url);
$container_map->{$container} = (object)array( $urls->{$url} = $ctx->{'@context'};
'@language' => new stdClass(), }
'@type' => new stdClass());
} // replace all URLS in the input
$entry = $container_map->{$container}; $this->_findContextUrls($input, $urls, true, $base);
}
if($mapping->reverse) {
// term is preferred for values using @reverse /**
$this->_addPreferredTerm( * Gets the initial context.
$mapping, $term, $entry->{'@type'}, '@reverse'); *
} else if(property_exists($mapping, '@type')) { * @param assoc $options the options to use.
// term is preferred for values using specific type * base the document base IRI.
$this->_addPreferredTerm( *
$mapping, $term, $entry->{'@type'}, $mapping->{'@type'}); * @return stdClass the initial context.
} else if(property_exists($mapping, '@language')) { */
// term is preferred for values using specific language protected function _getInitialContext($options)
$language = $mapping->{'@language'}; {
if($language === null) { return (object) [
$language = '@null'; '@base' => jsonld_parse_url($options['base']),
} 'mappings' => new stdClass(),
$this->_addPreferredTerm( 'inverse' => null];
$mapping, $term, $entry->{'@language'}, $language); }
} else {
// term is preferred for values w/default language or no type and /**
// no language * Generates an inverse context for use in the compaction algorithm, if
// add an entry for the default language * not already generated for the given active context.
$this->_addPreferredTerm( *
$mapping, $term, $entry->{'@language'}, $default_language); * @param stdClass $active_ctx the active context to use.
*
// add entries for no type and no language * @return stdClass the inverse context.
$this->_addPreferredTerm( */
$mapping, $term, $entry->{'@type'}, '@none'); protected function _getInverseContext($active_ctx)
$this->_addPreferredTerm( {
$mapping, $term, $entry->{'@language'}, '@none'); // inverse context already generated
} if ($active_ctx->inverse) {
} return $active_ctx->inverse;
} }
return $inverse; $inverse = $active_ctx->inverse = new stdClass();
}
// variables for building fast CURIE map
/** $fast_curie_map = $active_ctx->fast_curie_map = new ArrayObject();
* Adds the term for the given entry if not already added. $iris_to_terms = [];
*
* @param stdClass $mapping the term mapping. // handle default language
* @param string $term the term to add. $default_language = '@none';
* @param stdClass $entry the inverse context type_or_language entry to if (property_exists($active_ctx, '@language')) {
* add to. $default_language = $active_ctx->{'@language'};
* @param string $type_or_language_value the key in the entry to add to. }
*/
function _addPreferredTerm($mapping, $term, $entry, $type_or_language_value) { // create term selections for each mapping in the context, ordered by
if(!property_exists($entry, $type_or_language_value)) { // shortest and then lexicographically least
$entry->{$type_or_language_value} = $term; $mappings = $active_ctx->mappings;
} $terms = array_keys((array) $mappings);
} usort($terms, [$this, '_compareShortestLeast']);
foreach ($terms as $term) {
/** $mapping = $mappings->{$term};
* Clones an active context, creating a child active context. if ($mapping === null) {
* continue;
* @return stdClass a clone (child) of the active context. }
*/
protected function _cloneActiveContext($active_ctx) { // add term selection where it applies
$child = new stdClass(); if (property_exists($mapping, '@container')) {
$child->{'@base'} = $active_ctx->{'@base'}; $container = $mapping->{'@container'};
$child->mappings = self::copy($active_ctx->mappings); } else {
$child->inverse = null; $container = '@none';
if(property_exists($active_ctx, '@language')) { }
$child->{'@language'} = $active_ctx->{'@language'};
} // iterate over every IRI in the mapping
if(property_exists($active_ctx, '@vocab')) { $iris = $mapping->{'@id'};
$child->{'@vocab'} = $active_ctx->{'@vocab'}; $iris = self::arrayify($iris);
} foreach ($iris as $iri) {
return $child; $is_keyword = self::_isKeyword($iri);
}
// initialize container map
/** if (!property_exists($inverse, $iri)) {
* Returns whether or not the given value is a keyword. $inverse->{$iri} = new stdClass();
* if (!$is_keyword && !$mapping->_term_has_colon) {
* @param string $v the value to check. // init IRI to term map and fast CURIE map
* $iris_to_terms[$iri] = new ArrayObject();
* @return bool true if the value is a keyword, false if not. $iris_to_terms[$iri][] = $term;
*/ $fast_curie_entry = (object) [
protected static function _isKeyword($v) { 'iri' => $iri, 'terms' => $iris_to_terms[$iri]];
if(!is_string($v)) { if (!array_key_exists($iri[0], (array) $fast_curie_map)) {
return false; $fast_curie_map[$iri[0]] = new ArrayObject();
} }
switch($v) { $fast_curie_map[$iri[0]][] = $fast_curie_entry;
case '@base': }
case '@context': } else if (!$is_keyword && !$mapping->_term_has_colon) {
case '@container': // add IRI to term match
case '@default': $iris_to_terms[$iri][] = $term;
case '@embed': }
case '@explicit': $container_map = $inverse->{$iri};
case '@graph':
case '@id': // add new entry
case '@index': if (!property_exists($container_map, $container)) {
case '@language': $container_map->{$container} = (object) [
case '@list': '@language' => new stdClass(),
case '@omitDefault': '@type' => new stdClass()];
case '@preserve': }
case '@reverse': $entry = $container_map->{$container};
case '@set':
case '@type': if ($mapping->reverse) {
case '@value': // term is preferred for values using @reverse
case '@vocab': $this->_addPreferredTerm(
return true; $mapping, $term, $entry->{'@type'}, '@reverse');
} } else if (property_exists($mapping, '@type')) {
return false; // term is preferred for values using specific type
} $this->_addPreferredTerm(
$mapping, $term, $entry->{'@type'}, $mapping->{'@type'});
/** } else if (property_exists($mapping, '@language')) {
* Returns true if the given value is an empty Object. // term is preferred for values using specific language
* $language = $mapping->{'@language'};
* @param mixed $v the value to check. if ($language === null) {
* $language = '@null';
* @return bool true if the value is an empty Object, false if not. }
*/ $this->_addPreferredTerm(
protected static function _isEmptyObject($v) { $mapping, $term, $entry->{'@language'}, $language);
return is_object($v) && count(get_object_vars($v)) === 0; } else {
} // term is preferred for values w/default language or no type and
// no language
/** // add an entry for the default language
* Throws an exception if the given value is not a valid @type value. $this->_addPreferredTerm(
* $mapping, $term, $entry->{'@language'}, $default_language);
* @param mixed $v the value to check.
*/ // add entries for no type and no language
protected static function _validateTypeValue($v) { $this->_addPreferredTerm(
// must be a string or empty object $mapping, $term, $entry->{'@type'}, '@none');
if(is_string($v) || self::_isEmptyObject($v)) { $this->_addPreferredTerm(
return; $mapping, $term, $entry->{'@language'}, '@none');
} }
}
// must be an array }
$is_valid = false;
if(is_array($v)) { // build fast CURIE map
// must contain only strings foreach ($fast_curie_map as $key => $value) {
$is_valid = true; $this->_buildIriMap($fast_curie_map, $key, 1);
foreach($v as $e) { }
if(!(is_string($e))) {
$is_valid = false; return $inverse;
break; }
}
} /**
} * Runs a recursive algorithm to build a lookup map for quickly finding
* potential CURIEs.
if(!$is_valid) { *
throw new JsonLdException( * @param ArrayObject $iri_map the map to build.
'Invalid JSON-LD syntax; "@type" value must a string, an array ' . * @param string $key the current key in the map to work on.
'of strings, or an empty object.', * @param int $idx the index into the IRI to compare.
'jsonld.SyntaxError', 'invalid type value', array('value' => $v)); */
} function _buildIriMap($iri_map, $key, $idx)
} {
$entries = $iri_map[$key];
/** $next = $iri_map[$key] = new ArrayObject();
* Returns true if the given value is a subject with properties.
* foreach ($entries as $entry) {
* @param mixed $v the value to check. $iri = $entry->iri;
* if ($idx >= strlen($iri)) {
* @return bool true if the value is a subject with properties, false if not. $letter = '';
*/ } else {
protected static function _isSubject($v) { $letter = $iri[$idx];
// Note: A value is a subject if all of these hold true: }
// 1. It is an Object. if (!isset($next[$letter])) {
// 2. It is not a @value, @set, or @list. $next[$letter] = new ArrayObject();
// 3. It has more than 1 key OR any existing key is not @id. }
$rval = false; $next[$letter][] = $entry;
if(is_object($v) && }
!property_exists($v, '@value') &&
!property_exists($v, '@set') && foreach ($next as $key => $value) {
!property_exists($v, '@list')) { if ($key === '') {
$count = count(get_object_vars($v)); continue;
$rval = ($count > 1 || !property_exists($v, '@id')); }
} $this->_buildIriMap($next, $key, $idx + 1);
return $rval; }
} }
/** /**
* Returns true if the given value is a subject reference. * Adds the term for the given entry if not already added.
* *
* @param mixed $v the value to check. * @param stdClass $mapping the term mapping.
* * @param string $term the term to add.
* @return bool true if the value is a subject reference, false if not. * @param stdClass $entry the inverse context type_or_language entry to
*/ * add to.
protected static function _isSubjectReference($v) { * @param string $type_or_language_value the key in the entry to add to.
// Note: A value is a subject reference if all of these hold true: */
// 1. It is an Object. function _addPreferredTerm($mapping, $term, $entry, $type_or_language_value)
// 2. It has a single key: @id. {
return (is_object($v) && count(get_object_vars($v)) === 1 && if (!property_exists($entry, $type_or_language_value)) {
property_exists($v, '@id')); $entry->{$type_or_language_value} = $term;
} }
}
/**
* Returns true if the given value is a @value. /**
* * Clones an active context, creating a child active context.
* @param mixed $v the value to check. *
* * @return stdClass a clone (child) of the active context.
* @return bool true if the value is a @value, false if not. */
*/ protected function _cloneActiveContext($active_ctx)
protected static function _isValue($v) { {
// Note: A value is a @value if all of these hold true: $child = new stdClass();
// 1. It is an Object. $child->{'@base'} = $active_ctx->{'@base'};
// 2. It has the @value property. $child->mappings = self::copy($active_ctx->mappings);
return is_object($v) && property_exists($v, '@value'); $child->inverse = null;
} if (property_exists($active_ctx, '@language')) {
$child->{'@language'} = $active_ctx->{'@language'};
/** }
* Returns true if the given value is a @list. if (property_exists($active_ctx, '@vocab')) {
* $child->{'@vocab'} = $active_ctx->{'@vocab'};
* @param mixed $v the value to check. }
* return $child;
* @return bool true if the value is a @list, false if not. }
*/
protected static function _isList($v) { /**
// Note: A value is a @list if all of these hold true: * Returns whether or not the given value is a keyword.
// 1. It is an Object. *
// 2. It has the @list property. * @param string $v the value to check.
return is_object($v) && property_exists($v, '@list'); *
} * @return bool true if the value is a keyword, false if not.
*/
/** protected static function _isKeyword($v)
* Returns true if the given value is a blank node. {
* if (!is_string($v)) {
* @param mixed $v the value to check. return false;
* }
* @return bool true if the value is a blank node, false if not. switch ($v) {
*/ case '@base':
protected static function _isBlankNode($v) { case '@context':
// Note: A value is a blank node if all of these hold true: case '@container':
// 1. It is an Object. case '@default':
// 2. If it has an @id key its value begins with '_:'. case '@embed':
// 3. It has no keys OR is not a @value, @set, or @list. case '@explicit':
$rval = false; case '@graph':
if(is_object($v)) { case '@id':
if(property_exists($v, '@id')) { case '@index':
$rval = (strpos($v->{'@id'}, '_:') === 0); case '@language':
} else { case '@list':
$rval = (count(get_object_vars($v)) === 0 || case '@omitDefault':
!(property_exists($v, '@value') || case '@preserve':
property_exists($v, '@set') || case '@requireAll':
property_exists($v, '@list'))); case '@reverse':
} case '@set':
} case '@type':
return $rval; case '@value':
} case '@vocab':
return true;
/** }
* Returns true if the given value is an absolute IRI, false if not. return false;
* }
* @param string $v the value to check.
* /**
* @return bool true if the value is an absolute IRI, false if not. * Returns true if the given value is an empty Object.
*/ *
protected static function _isAbsoluteIri($v) { * @param mixed $v the value to check.
return strpos($v, ':') !== false; *
} * @return bool true if the value is an empty Object, false if not.
*/
/** protected static function _isEmptyObject($v)
* Returns true if the given target has the given key and its {
* value equals is the given value. return is_object($v) && count(get_object_vars($v)) === 0;
* }
* @param stdClass $target the target object.
* @param string key the key to check. /**
* @param mixed $value the value to check. * Throws an exception if the given value is not a valid @type value.
* *
* @return bool true if the target has the given key and its value matches. * @param mixed $v the value to check.
*/ */
protected static function _hasKeyValue($target, $key, $value) { protected static function _validateTypeValue($v)
return (property_exists($target, $key) && $target->{$key} === $value); {
} // must be a string or empty object
if (is_string($v) || self::_isEmptyObject($v)) {
/** return;
* Returns true if both of the given objects have the same value for the }
* given key or if neither of the objects contain the given key.
* // must be an array
* @param stdClass $o1 the first object. $is_valid = false;
* @param stdClass $o2 the second object. if (is_array($v)) {
* @param string key the key to check. // must contain only strings
* $is_valid = true;
* @return bool true if both objects have the same value for the key or foreach ($v as $e) {
* neither has the key. if (!(is_string($e))) {
*/ $is_valid = false;
protected static function _compareKeyValues($o1, $o2, $key) { break;
if(property_exists($o1, $key)) { }
return property_exists($o2, $key) && $o1->{$key} === $o2->{$key}; }
} }
return !property_exists($o2, $key);
} if (!$is_valid) {
throw new JsonLdException(
/** 'Invalid JSON-LD syntax; "@type" value must a string, an array ' .
* Parses JSON and sets an appropriate exception message on error. 'of strings, or an empty object.', 'jsonld.SyntaxError', 'invalid type value', ['value' => $v]);
* }
* @param string $json the JSON to parse. }
*
* @return mixed the parsed JSON object or array. /**
*/ * Returns true if the given value is a subject with properties.
protected static function _parse_json($json) { *
$rval = json_decode($json); * @param mixed $v the value to check.
$error = json_last_error(); *
if($error === JSON_ERROR_NONE && $rval === null) { * @return bool true if the value is a subject with properties, false if not.
$error = JSON_ERROR_SYNTAX; */
} protected static function _isSubject($v)
switch($error) { {
case JSON_ERROR_NONE: // Note: A value is a subject if all of these hold true:
break; // 1. It is an Object.
case JSON_ERROR_DEPTH: // 2. It is not a @value, @set, or @list.
throw new JsonLdException( // 3. It has more than 1 key OR any existing key is not @id.
'Could not parse JSON; the maximum stack depth has been exceeded.', $rval = false;
'jsonld.ParseError'); if (is_object($v) &&
case JSON_ERROR_STATE_MISMATCH: !property_exists($v, '@value') &&
throw new JsonLdException( !property_exists($v, '@set') &&
'Could not parse JSON; invalid or malformed JSON.', !property_exists($v, '@list')) {
'jsonld.ParseError'); $count = count(get_object_vars($v));
case JSON_ERROR_CTRL_CHAR: $rval = ($count > 1 || !property_exists($v, '@id'));
case JSON_ERROR_SYNTAX: }
throw new JsonLdException( return $rval;
'Could not parse JSON; syntax error, malformed JSON.', }
'jsonld.ParseError');
case JSON_ERROR_UTF8: /**
throw new JsonLdException( * Returns true if the given value is a subject reference.
'Could not parse JSON from URL; malformed UTF-8 characters.', *
'jsonld.ParseError'); * @param mixed $v the value to check.
default: *
throw new JsonLdException( * @return bool true if the value is a subject reference, false if not.
'Could not parse JSON from URL; unknown error.', */
'jsonld.ParseError'); protected static function _isSubjectReference($v)
} {
return $rval; // Note: A value is a subject reference if all of these hold true:
} // 1. It is an Object.
// 2. It has a single key: @id.
return (is_object($v) && count(get_object_vars($v)) === 1 &&
property_exists($v, '@id'));
}
/**
* Returns true if the given value is a @value.
*
* @param mixed $v the value to check.
*
* @return bool true if the value is a @value, false if not.
*/
protected static function _isValue($v)
{
// Note: A value is a @value if all of these hold true:
// 1. It is an Object.
// 2. It has the @value property.
return is_object($v) && property_exists($v, '@value');
}
/**
* Returns true if the given value is a @list.
*
* @param mixed $v the value to check.
*
* @return bool true if the value is a @list, false if not.
*/
protected static function _isList($v)
{
// Note: A value is a @list if all of these hold true:
// 1. It is an Object.
// 2. It has the @list property.
return is_object($v) && property_exists($v, '@list');
}
/**
* Returns true if the given value is a blank node.
*
* @param mixed $v the value to check.
*
* @return bool true if the value is a blank node, false if not.
*/
protected static function _isBlankNode($v)
{
// Note: A value is a blank node if all of these hold true:
// 1. It is an Object.
// 2. If it has an @id key its value begins with '_:'.
// 3. It has no keys OR is not a @value, @set, or @list.
$rval = false;
if (is_object($v)) {
if (property_exists($v, '@id')) {
$rval = (strpos($v->{'@id'}, '_:') === 0);
} else {
$rval = (count(get_object_vars($v)) === 0 ||
!(property_exists($v, '@value') ||
property_exists($v, '@set') ||
property_exists($v, '@list')));
}
}
return $rval;
}
/**
* Returns true if the given value is an absolute IRI, false if not.
*
* @param string $v the value to check.
*
* @return bool true if the value is an absolute IRI, false if not.
*/
protected static function _isAbsoluteIri($v)
{
return strpos($v, ':') !== false;
}
/**
* Returns true if the given target has the given key and its
* value equals is the given value.
*
* @param stdClass $target the target object.
* @param string key the key to check.
* @param mixed $value the value to check.
*
* @return bool true if the target has the given key and its value matches.
*/
protected static function _hasKeyValue($target, $key, $value)
{
return (property_exists($target, $key) && $target->{$key} === $value);
}
/**
* Returns true if both of the given objects have the same value for the
* given key or if neither of the objects contain the given key.
*
* @param stdClass $o1 the first object.
* @param stdClass $o2 the second object.
* @param string key the key to check.
*
* @return bool true if both objects have the same value for the key or
* neither has the key.
*/
protected static function _compareKeyValues($o1, $o2, $key)
{
if (property_exists($o1, $key)) {
return property_exists($o2, $key) && $o1->{$key} === $o2->{$key};
}
return !property_exists($o2, $key);
}
/**
* Parses JSON and sets an appropriate exception message on error.
*
* @param string $json the JSON to parse.
*
* @return mixed the parsed JSON object or array.
*/
protected static function _parse_json($json)
{
$rval = json_decode($json);
$error = json_last_error();
if ($error === JSON_ERROR_NONE && $rval === null) {
$error = JSON_ERROR_SYNTAX;
}
switch ($error) {
case JSON_ERROR_NONE:
break;
case JSON_ERROR_DEPTH:
throw new JsonLdException(
'Could not parse JSON; the maximum stack depth has been exceeded.', 'jsonld.ParseError');
case JSON_ERROR_STATE_MISMATCH:
throw new JsonLdException(
'Could not parse JSON; invalid or malformed JSON.', 'jsonld.ParseError');
case JSON_ERROR_CTRL_CHAR:
case JSON_ERROR_SYNTAX:
throw new JsonLdException(
'Could not parse JSON; syntax error, malformed JSON.', 'jsonld.ParseError');
case JSON_ERROR_UTF8:
throw new JsonLdException(
'Could not parse JSON from URL; malformed UTF-8 characters.', 'jsonld.ParseError');
default:
throw new JsonLdException(
'Could not parse JSON from URL; unknown error.', 'jsonld.ParseError');
}
return $rval;
}
} }
// register the N-Quads RDF parser // register the N-Quads RDF parser
jsonld_register_rdf_parser( jsonld_register_rdf_parser(
'application/nquads', array('JsonLdProcessor', 'parseNQuads')); 'application/nquads', ['JsonLdProcessor', 'parseNQuads']);
/** /**
* A JSON-LD Exception. * A JSON-LD Exception.
*/ */
class JsonLdException extends Exception { class JsonLdException extends Exception
public function __construct( {
$msg, $type, $code='error', $details=null, $previous=null) {
$this->type = $type; public function __construct(
$this->code = $code; $msg, $type, $code = 'error', $details = null, $previous = null)
$this->details = $details; {
$this->cause = $previous; $this->type = $type;
parent::__construct($msg, 0, $previous); $this->code = $code;
} $this->details = $details;
public function __toString() { $this->cause = $previous;
$rval = __CLASS__ . ": [{$this->type}]: {$this->message}\n"; parent::__construct($msg, 0, $previous);
if($this->code) { }
$rval .= 'Code: ' . $this->code . "\n";
} public function __toString()
if($this->details) { {
$rval .= 'Details: ' . print_r($this->details, true) . "\n"; $rval = __CLASS__ . ": [{$this->type}]: {$this->message}\n";
} if ($this->code) {
if($this->cause) { $rval .= 'Code: ' . $this->code . "\n";
$rval .= 'Cause: ' . $this->cause; }
} if ($this->details) {
$rval .= $this->getTraceAsString() . "\n"; $rval .= 'Details: ' . print_r($this->details, true) . "\n";
return $rval; }
} if ($this->cause) {
}; $rval .= 'Cause: ' . $this->cause;
}
$rval .= $this->getTraceAsString() . "\n";
return $rval;
}
}
;
/** /**
* A UniqueNamer issues unique names, keeping track of any previously issued * A UniqueNamer issues unique names, keeping track of any previously issued
* names. * names.
*/ */
class UniqueNamer { class UniqueNamer
/** {
* Constructs a new UniqueNamer.
*
* @param prefix the prefix to use ('<prefix><counter>').
*/
public function __construct($prefix) {
$this->prefix = $prefix;
$this->counter = 0;
$this->existing = new stdClass();
$this->order = array();
}
/** /**
* Clones this UniqueNamer. * Constructs a new UniqueNamer.
*/ *
public function __clone() { * @param prefix the prefix to use ('<prefix><counter>').
$this->existing = clone $this->existing; */
} public function __construct($prefix)
{
$this->prefix = $prefix;
$this->counter = 0;
$this->existing = new stdClass();
$this->order = [];
}
/** /**
* Gets the new name for the given old name, where if no old name is given * Clones this UniqueNamer.
* a new name will be generated. */
* public function __clone()
* @param mixed [$old_name] the old name to get the new name for. {
* $this->existing = clone $this->existing;
* @return string the new name. }
*/
public function getName($old_name=null) {
// return existing old name
if($old_name && property_exists($this->existing, $old_name)) {
return $this->existing->{$old_name};
}
// get next name /**
$name = $this->prefix . $this->counter; * Gets the new name for the given old name, where if no old name is given
$this->counter += 1; * a new name will be generated.
*
* @param mixed [$old_name] the old name to get the new name for.
*
* @return string the new name.
*/
public function getName($old_name = null)
{
// return existing old name
if ($old_name && property_exists($this->existing, $old_name)) {
return $this->existing->{$old_name};
}
// save mapping // get next name
if($old_name !== null) { $name = $this->prefix . $this->counter;
$this->existing->{$old_name} = $name; $this->counter += 1;
$this->order[] = $old_name;
}
return $name; // save mapping
} if ($old_name !== null) {
$this->existing->{$old_name} = $name;
$this->order[] = $old_name;
}
return $name;
}
/**
* Returns true if the given old name has already been assigned a new name.
*
* @param string $old_name the old name to check.
*
* @return true if the old name has been assigned a new name, false if not.
*/
public function isNamed($old_name)
{
return property_exists($this->existing, $old_name);
}
/**
* Returns true if the given old name has already been assigned a new name.
*
* @param string $old_name the old name to check.
*
* @return true if the old name has been assigned a new name, false if not.
*/
public function isNamed($old_name) {
return property_exists($this->existing, $old_name);
}
} }
/** /**
* A Permutator iterates over all possible permutations of the given array * A Permutator iterates over all possible permutations of the given array
* of elements. * of elements.
*/ */
class Permutator { class Permutator
/** {
* Constructs a new Permutator.
*
* @param array $list the array of elements to iterate over.
*/
public function __construct($list) {
// original array
$this->list = $list;
sort($this->list);
// indicates whether there are more permutations
$this->done = false;
// directional info for permutation algorithm
$this->left = new stdClass();
foreach($list as $v) {
$this->left->{$v} = true;
}
}
/** /**
* Returns true if there is another permutation. * Constructs a new Permutator.
* *
* @return bool true if there is another permutation, false if not. * @param array $list the array of elements to iterate over.
*/ */
public function hasNext() { public function __construct($list)
return !$this->done; {
} // original array
$this->list = $list;
sort($this->list);
// indicates whether there are more permutations
$this->done = false;
// directional info for permutation algorithm
$this->left = new stdClass();
foreach ($list as $v) {
$this->left->{$v} = true;
}
}
/** /**
* Gets the next permutation. Call hasNext() to ensure there is another one * Returns true if there is another permutation.
* first. *
* * @return bool true if there is another permutation, false if not.
* @return array the next permutation. */
*/ public function hasNext()
public function next() { {
// copy current permutation return !$this->done;
$rval = $this->list; }
/* Calculate the next permutation using the Steinhaus-Johnson-Trotter /**
permutation algorithm. */ * Gets the next permutation. Call hasNext() to ensure there is another one
* first.
*
* @return array the next permutation.
*/
public function next()
{
// copy current permutation
$rval = $this->list;
// get largest mobile element k /* Calculate the next permutation using the Steinhaus-Johnson-Trotter
// (mobile: element is greater than the one it is looking at) permutation algorithm. */
$k = null;
$pos = 0;
$length = count($this->list);
for($i = 0; $i < $length; ++$i) {
$element = $this->list[$i];
$left = $this->left->{$element};
if(($k === null || $element > $k) &&
(($left && $i > 0 && $element > $this->list[$i - 1]) ||
(!$left && $i < ($length - 1) && $element > $this->list[$i + 1]))) {
$k = $element;
$pos = $i;
}
}
// no more permutations // get largest mobile element k
if($k === null) { // (mobile: element is greater than the one it is looking at)
$this->done = true; $k = null;
} else { $pos = 0;
// swap k and the element it is looking at $length = count($this->list);
$swap = $this->left->{$k} ? $pos - 1 : $pos + 1; for ($i = 0; $i < $length; ++$i) {
$this->list[$pos] = $this->list[$swap]; $element = $this->list[$i];
$this->list[$swap] = $k; $left = $this->left->{$element};
if (($k === null || $element > $k) &&
(($left && $i > 0 && $element > $this->list[$i - 1]) ||
(!$left && $i < ($length - 1) && $element > $this->list[$i + 1]))) {
$k = $element;
$pos = $i;
}
}
// reverse the direction of all elements larger than k // no more permutations
for($i = 0; $i < $length; ++$i) { if ($k === null) {
if($this->list[$i] > $k) { $this->done = true;
$this->left->{$this->list[$i]} = !$this->left->{$this->list[$i]}; } else {
} // swap k and the element it is looking at
} $swap = $this->left->{$k} ? $pos - 1 : $pos + 1;
} $this->list[$pos] = $this->list[$swap];
$this->list[$swap] = $k;
// reverse the direction of all elements larger than k
for ($i = 0; $i < $length; ++$i) {
if ($this->list[$i] > $k) {
$this->left->{$this->list[$i]} = !$this->left->{$this->list[$i]};
}
}
}
return $rval;
}
return $rval;
}
} }
/** /**
* An ActiveContextCache caches active contexts so they can be reused without * An ActiveContextCache caches active contexts so they can be reused without
* the overhead of recomputing them. * the overhead of recomputing them.
*/ */
class ActiveContextCache { class ActiveContextCache
/** {
* Constructs a new ActiveContextCache. /**
* * Constructs a new ActiveContextCache.
* @param int size the maximum size of the cache, defaults to 100. *
*/ * @param int size the maximum size of the cache, defaults to 100.
public function __construct($size=100) { */
$this->order = array(); public function __construct($size = 100)
$this->cache = new stdClass(); {
$this->size = $size; $this->order = [];
} $this->cache = new stdClass();
$this->size = $size;
}
/** /**
* Gets an active context from the cache based on the current active * Gets an active context from the cache based on the current active
* context and the new local context. * context and the new local context.
* *
* @param stdClass $active_ctx the current active context. * @param stdClass $active_ctx the current active context.
* @param stdClass $local_ctx the new local context. * @param stdClass $local_ctx the new local context.
* *
* @return mixed a shared copy of the cached active context or null. * @return mixed a shared copy of the cached active context or null.
*/ */
public function get($active_ctx, $local_ctx) { public function get($active_ctx, $local_ctx)
$key1 = serialize($active_ctx); {
$key2 = serialize($local_ctx); $key1 = serialize($active_ctx);
if(property_exists($this->cache, $key1)) { $key2 = serialize($local_ctx);
$level1 = $this->cache->{$key1}; if (property_exists($this->cache, $key1)) {
if(property_exists($level1, $key2)) { $level1 = $this->cache->{$key1};
return $level1->{$key2}; if (property_exists($level1, $key2)) {
} return $level1->{$key2};
} }
return null; }
}
/** return null;
* Sets an active context in the cache based on the previous active }
* context and the just-processed local context.
* /**
* @param stdClass $active_ctx the previous active context. * Sets an active context in the cache based on the previous active
* @param stdClass $local_ctx the just-processed local context. * context and the just-processed local context.
* @param stdClass $result the resulting active context. *
*/ * @param stdClass $active_ctx the previous active context.
public function set($active_ctx, $local_ctx, $result) { * @param stdClass $local_ctx the just-processed local context.
if(count($this->order) === $this->size) { * @param stdClass $result the resulting active context.
$entry = array_shift($this->order); */
unset($this->cache->{$entry->activeCtx}->{$entry->localCtx}); public function set($active_ctx, $local_ctx, $result)
} {
$key1 = serialize($active_ctx); if (count($this->order) === $this->size) {
$key2 = serialize($local_ctx); $entry = array_shift($this->order);
$this->order[] = (object)array( unset($this->cache->{$entry->activeCtx}->{$entry->localCtx});
'activeCtx' => $key1, 'localCtx' => $key2); }
if(!property_exists($this->cache, $key1)) {
$this->cache->{$key1} = new stdClass(); $key1 = serialize($active_ctx);
} $key2 = serialize($local_ctx);
$this->cache->{$key1}->{$key2} = JsonLdProcessor::copy($result); $this->order[] = (object) [
} 'activeCtx' => $key1, 'localCtx' => $key2];
if (!property_exists($this->cache, $key1)) {
$this->cache->{$key1} = new stdClass();
}
$this->cache->{$key1}->{$key2} = JsonLdProcessor::copy($result);
}
} }
/* end of file, omit ?> */ /* end of file, omit ?> */

1303
test.php
View file

@ -1,4 +1,5 @@
<?php <?php
/** /**
* PHP unit tests for JSON-LD. * PHP unit tests for JSON-LD.
* *
@ -8,639 +9,841 @@
*/ */
require_once('jsonld.php'); require_once('jsonld.php');
class JsonLdTestCase extends PHPUnit_Framework_TestCase { class JsonLdTestCase extends PHPUnit_Framework_TestCase
/** {
* Runs this test case. Overridden to attach to EARL report w/o need for /**
* an external XML configuration file. * Runs this test case. Overridden to attach to EARL report w/o need for
* * an external XML configuration file.
* @param PHPUnit_Framework_TestResult $result the test result. *
*/ * @param PHPUnit_Framework_TestResult $result the test result.
public function run(PHPUnit_Framework_TestResult $result = NULL) { */
global $EARL; public function run(PHPUnit_Framework_TestResult $result = NULL)
$EARL->attach($result); {
$this->result = $result; global $EARL;
parent::run($result); $EARL->attach($result);
} $this->result = $result;
parent::run($result);
}
/** /**
* Tests expansion. * Tests expansion.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group expand * @group expand
* @dataProvider expandProvider * @group json-ld.org
*/ * @dataProvider expandProvider
public function testExpand($test) { */
$this->test = $test; public function testExpand($test)
$input = $test->readUrl('input'); {
$options = $test->createOptions(); $this->test = $test;
$test->run('jsonld_expand', array($input, $options)); $input = $test->readUrl('input');
} $options = $test->createOptions();
$test->run('jsonld_expand', [$input, $options]);
}
/** /**
* Tests compaction. * Tests compaction.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group compact * @group compact
* @dataProvider compactProvider * @group json-ld.org
*/ * @dataProvider compactProvider
public function testCompact($test) { */
$this->test = $test; public function testCompact($test)
$input = $test->readUrl('input'); {
$context = $test->readProperty('context'); $this->test = $test;
$options = $test->createOptions(); $input = $test->readUrl('input');
$test->run('jsonld_compact', array($input, $context, $options)); $context = $test->readProperty('context');
} $options = $test->createOptions();
$test->run('jsonld_compact', [$input, $context, $options]);
}
/** /**
* Tests flatten. * Tests flatten.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group flatten * @group flatten
* @dataProvider flattenProvider * @group json-ld.org
*/ * @dataProvider flattenProvider
public function testFlatten($test) { */
$this->test = $test; public function testFlatten($test)
$input = $test->readUrl('input'); {
$context = $test->readProperty('context'); $this->test = $test;
$options = $test->createOptions(); $input = $test->readUrl('input');
$test->run('jsonld_flatten', array($input, $context, $options)); $context = $test->readProperty('context');
} $options = $test->createOptions();
$test->run('jsonld_flatten', [$input, $context, $options]);
}
/** /**
* Tests serialization to RDF. * Tests serialization to RDF.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group toRdf * @group toRdf
* @dataProvider toRdfProvider * @group json-ld.org
*/ * @dataProvider toRdfProvider
public function testToRdf($test) { */
$this->test = $test; public function testToRdf($test)
$input = $test->readUrl('input'); {
$options = $test->createOptions(array('format' => 'application/nquads')); $this->test = $test;
$test->run('jsonld_to_rdf', array($input, $options)); $input = $test->readUrl('input');
} $options = $test->createOptions(['format' => 'application/nquads']);
$test->run('jsonld_to_rdf', [$input, $options]);
}
/** /**
* Tests deserialization from RDF. * Tests deserialization from RDF.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group fromRdf * @group fromRdf
* @dataProvider fromRdfProvider * @group json-ld.org
*/ * @dataProvider fromRdfProvider
public function testFromRdf($test) { */
$this->test = $test; public function testFromRdf($test)
$input = $test->readProperty('input'); {
$options = $test->createOptions(array('format' => 'application/nquads')); $this->test = $test;
$test->run('jsonld_from_rdf', array($input, $options)); $input = $test->readProperty('input');
} $options = $test->createOptions(['format' => 'application/nquads']);
$test->run('jsonld_from_rdf', [$input, $options]);
}
/** /**
* Tests framing. * Tests framing.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group frame * @group frame
* @dataProvider frameProvider * @group json-ld.org
*/ * @dataProvider frameProvider
public function testFrame($test) { */
$this->test = $test; public function testFrame($test)
$input = $test->readUrl('input'); {
$frame = $test->readProperty('frame'); $this->test = $test;
$options = $test->createOptions(); $input = $test->readUrl('input');
$test->run('jsonld_frame', array($input, $frame, $options)); $frame = $test->readProperty('frame');
} $options = $test->createOptions();
$test->run('jsonld_frame', [$input, $frame, $options]);
}
/** /**
* Tests normalization. * Tests normalization.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group normalize * @group normalize
* @dataProvider normalizeProvider * @group json-ld.org
*/ * @dataProvider normalizeProvider
public function testNormalize($test) { */
$this->test = $test; public function testNormalize($test)
$input = $test->readUrl('input'); {
$options = $test->createOptions(array('format' => 'application/nquads')); $this->test = $test;
$test->run('jsonld_normalize', array($input, $options)); $input = $test->readUrl('input');
} $options = $test->createOptions(['format' => 'application/nquads']);
$test->run('jsonld_normalize', [$input, $options]);
}
public function expandProvider() { /**
return new JsonLdTestIterator('jld:ExpandTest'); * Tests URGNA2012 normalization.
} *
* @param JsonLdTest $test the test to run.
*
* @group normalize
* @group normalization
* @dataProvider urgna2012Provider
*/
public function testUrgna2012($test)
{
$this->test = $test;
$input = $test->readProperty('action');
$options = $test->createOptions([
'algorithm' => 'URGNA2012',
'inputFormat' => 'application/nquads',
'format' => 'application/nquads']);
$test->run('jsonld_normalize', [$input, $options]);
}
public function compactProvider() { /**
return new JsonLdTestIterator('jld:CompactTest'); * Tests URDNA2015 normalization.
} *
* @param JsonLdTest $test the test to run.
*
* @group normalize
* @group normalization
* @dataProvider urdna2015Provider
*/
public function testUrdna2015($test)
{
$this->test = $test;
$input = $test->readProperty('action');
$options = $test->createOptions([
'algorithm' => 'URDNA2015',
'inputFormat' => 'application/nquads',
'format' => 'application/nquads']);
$test->run('jsonld_normalize', [$input, $options]);
}
public function flattenProvider() { public function expandProvider()
return new JsonLdTestIterator('jld:FlattenTest'); {
} return new JsonLdTestIterator('jld:ExpandTest');
}
public function toRdfProvider() { public function compactProvider()
return new JsonLdTestIterator('jld:ToRDFTest'); {
} return new JsonLdTestIterator('jld:CompactTest');
}
public function fromRdfProvider() { public function flattenProvider()
return new JsonLdTestIterator('jld:FromRDFTest'); {
} return new JsonLdTestIterator('jld:FlattenTest');
}
public function normalizeProvider() { public function toRdfProvider()
return new JsonLdTestIterator('jld:NormalizeTest'); {
} return new JsonLdTestIterator('jld:ToRDFTest');
}
public function frameProvider() { public function fromRdfProvider()
return new JsonLdTestIterator('jld:FrameTest'); {
} return new JsonLdTestIterator('jld:FromRDFTest');
}
public function normalizeProvider()
{
return new JsonLdTestIterator('jld:NormalizeTest');
}
public function frameProvider()
{
return new JsonLdTestIterator('jld:FrameTest');
}
public function urgna2012Provider()
{
return new JsonLdTestIterator('rdfn:Urgna2012EvalTest');
}
public function urdna2015Provider()
{
return new JsonLdTestIterator('rdfn:Urdna2015EvalTest');
}
} }
class JsonLdManifest { class JsonLdManifest
public function __construct($data, $filename) { {
$this->data = $data; public function __construct($data, $filename)
$this->filename = $filename; {
$this->dirname = dirname($filename); $this->data = $data;
} $this->filename = $filename;
$this->dirname = dirname($filename);
}
public function load(&$tests) { public function load(&$tests)
$sequence = JsonLdProcessor::getValues($this->data, 'sequence'); {
foreach($sequence as $entry) { $entries = array_merge(
if(is_string($entry)) { JsonLdProcessor::getValues($this->data, 'sequence'), JsonLdProcessor::getValues($this->data, 'entries'));
$filename = join( $includes = JsonLdProcessor::getValues($this->data, 'include');
DIRECTORY_SEPARATOR, array($this->dirname, $entry)); foreach ($includes as $include) {
$entry = Util::readJson($filename); array_push($entries, $include . '.jsonld');
} else { }
$filename = $this->filename; foreach ($entries as $entry) {
} if (is_string($entry)) {
$filename = join(
DIRECTORY_SEPARATOR, [$this->dirname, $entry]);
$entry = Util::readJson($filename);
} else {
$filename = $this->filename;
}
if(JsonLdProcessor::hasValue($entry, '@type', 'mf:Manifest')) { if (JsonLdProcessor::hasValue($entry, '@type', 'mf:Manifest') ||
// entry is another manifest JsonLdProcessor::hasValue($entry, 'type', 'mf:Manifest')) {
$manifest = new JsonLdManifest($entry, $filename); // entry is another manifest
$manifest->load($tests); $manifest = new JsonLdManifest($entry, $filename);
} else { $manifest->load($tests);
// assume entry is a test } else {
$test = new JsonLdTest($this, $entry, $filename); // assume entry is a test
$types = JsonLdProcessor::getValues($test->data, '@type'); $test = new JsonLdTest($this, $entry, $filename);
foreach($types as $type) { $types = array_merge(
if(!isset($tests[$type])) { JsonLdProcessor::getValues($test->data, '@type'), JsonLdProcessor::getValues($test->data, 'type'));
$tests[$type] = array(); foreach ($types as $type) {
} if (!isset($tests[$type])) {
$tests[$type][] = $test; $tests[$type] = [];
} }
} $tests[$type][] = $test;
} }
} }
}
}
} }
class JsonLdTest { class JsonLdTest
public function __construct($manifest, $data, $filename) { {
$this->manifest = $manifest; public function __construct($manifest, $data, $filename)
$this->data = $data; {
$this->filename = $filename; $this->manifest = $manifest;
$this->dirname = dirname($filename); $this->data = $data;
$this->isPositive = JsonLdProcessor::hasValue( $this->filename = $filename;
$data, '@type', 'jld:PositiveEvaluationTest'); $this->dirname = dirname($filename);
$this->isNegative = JsonLdProcessor::hasValue( $this->isPositive = JsonLdProcessor::hasValue(
$data, '@type', 'jld:NegativeEvaluationTest'); $data, '@type', 'jld:PositiveEvaluationTest') ||
JsonLdProcessor::hasValue(
$data, 'type', 'jld:PositiveEvaluationTest');
$this->isNegative = JsonLdProcessor::hasValue(
$data, '@type', 'jld:NegativeEvaluationTest') ||
JsonLdProcessor::hasValue(
$data, 'type', 'jld:NegativeEvaluationTest');
// generate test name // generate test name
$this->name = $manifest->data->name . ' ' . substr($data->{'@id'}, 2) . if (isset($manifest->data->name)) {
' - ' . $this->data->name; $manifestLabel = $manifest->data->name;
} else if (isset($manifest->data->label)) {
$manifestLabel = $manifest->data->label;
} else {
$manifestLabel = 'UNNAMED';
}
if (isset($this->data->id)) {
$testId = $this->data->id;
} else {
$testId = $this->data->{'@id'};
}
if (isset($this->data->name)) {
$testLabel = $this->data->name;
} else if (isset($this->data->label)) {
$testLabel = $this->data->label;
} else {
$testLabel = 'UNNAMED';
}
// expand @id and input base $this->name = $manifestLabel . ' ' . $testId . ' - ' . $testLabel;
$data->{'@id'} = ($manifest->data->baseIri .
basename($manifest->filename) . $data->{'@id'});
$this->base = $manifest->data->baseIri . $data->input;
}
public function run($fn, $params) { // expand @id and input base
// read expected data if (isset($manifest->data->baseIri)) {
if($this->isNegative) { $data->{'@id'} = ($manifest->data->baseIri .
$this->expected = $this->data->expect; basename($manifest->filename) . $data->{'@id'});
} else { $this->base = $manifest->data->baseIri . $data->input;
$this->expected = $this->readProperty('expect'); }
} }
try { private function _getResultProperty()
$this->actual = call_user_func_array($fn, $params); {
if($this->isNegative) { if (isset($this->data->expect)) {
throw new Exception('Expected an error; one was not raised.'); return 'expect';
} } else if (isset($this->data->result)) {
PHPUnit_Framework_TestCase::assertEquals($this->expected, $this->actual); return 'result';
} catch(Exception $e) { } else {
if($this->isPositive) { throw new Exception('No test result property found.');
throw $e; }
} }
$this->actual = $this->getJsonLdErrorCode($e);
PHPUnit_Framework_TestCase::assertEquals($this->expected, $this->actual);
}
}
public function readUrl($property) { public function run($fn, $params)
if(!property_exists($this->data, $property)) { {
return null; // read expected data
} if ($this->isNegative) {
return $this->manifest->data->baseIri . $this->data->{$property}; $this->expected = $this->data->expect;
} } else {
$this->expected = $this->readProperty($this->_getResultProperty());
}
public function readProperty($property) { try {
$data = $this->data; $this->actual = call_user_func_array($fn, $params);
if(!property_exists($data, $property)) { if ($this->isNegative) {
return null; throw new Exception('Expected an error; one was not raised.');
} }
$filename = join( PHPUnit_Framework_TestCase::assertEquals($this->expected, $this->actual);
DIRECTORY_SEPARATOR, array($this->dirname, $data->{$property})); } catch (Exception $e) {
$extension = pathinfo($filename, PATHINFO_EXTENSION); // assume positive test
if($extension === 'jsonld') { if ($this->isNegative) {
return Util::readJson($filename); $this->actual = $this->getJsonLdErrorCode($e);
} PHPUnit_Framework_TestCase::assertEquals(
return Util::readFile($filename); $this->expected, $this->actual);
} } else {
throw $e;
}
}
}
public function createOptions($opts=array()) { public function readUrl($property)
$http_options = array( {
'contentType', 'httpLink', 'httpStatus', 'redirectTo'); if (!property_exists($this->data, $property)) {
$test_options = (property_exists($this->data, 'option') ? return null;
$this->data->option : array()); }
$options = array(); return $this->manifest->data->baseIri . $this->data->{$property};
foreach($test_options as $k => $v) { }
if(!in_array($k, $http_options)) {
$options[$k] = $v;
}
}
$options['documentLoader'] = $this->createDocumentLoader();
$options = array_merge($options, $opts);
if(isset($options['expandContext'])) {
$filename = join(
DIRECTORY_SEPARATOR, array($this->dirname, $options['expandContext']));
$options['expandContext'] = Util::readJson($filename);
}
return $options;
}
public function createDocumentLoader() { public function readProperty($property)
$base = 'http://json-ld.org/test-suite'; {
$test = $this; $data = $this->data;
if (!property_exists($data, $property)) {
return null;
}
$filename = join(
DIRECTORY_SEPARATOR, [$this->dirname, $data->{$property}]);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
if ($extension === 'jsonld') {
return Util::readJson($filename);
}
$load_locally = function($url) use ($test, $base) { return Util::readFile($filename);
$doc = (object)array( }
'contextUrl' => null, 'documentUrl' => $url, 'document' => null);
$options = (property_exists($test->data, 'option') ?
$test->data->option : null);
if($options and $url === $test->base) {
if(property_exists($options, 'redirectTo') &&
property_exists($options, 'httpStatus') &&
$options->httpStatus >= '300') {
$doc->documentUrl = ($test->manifest->data->{'baseIri'} .
$options->redirectTo);
} else if(property_exists($options, 'httpLink')) {
$content_type = (property_exists($options, 'contentType') ?
$options->contentType : null);
$extension = pathinfo($url, PATHINFO_EXTENSION);
if(!$content_type && $extension === 'jsonld') {
$content_type = 'application/ld+json';
}
$link_header = $options->httpLink;
if(is_array($link_header)) {
$link_header = join(',', $link_header);
}
$link_header = jsonld_parse_link_header($link_header);
if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
$link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
} else {
$link_header = null;
}
if($link_header && $content_type !== 'application/ld+json') {
if(is_array($link_header)) {
throw new Exception('multiple context link headers');
}
$doc->{'contextUrl'} = $link_header->target;
}
}
}
global $ROOT_MANIFEST_DIR;
$filename = $ROOT_MANIFEST_DIR .
substr($doc->{'documentUrl'}, strlen($base));
try {
$doc->{'document'} = Util::readJson($filename);
} catch(Exception $e) {
throw new Exception('loading document failed');
}
return $doc;
};
$local_loader = function($url) use ($test, $base, $load_locally) { public function createOptions($opts = [])
// always load remote-doc and non-base tests remotely {
if(strpos($url, $base) !== 0 || $http_options = [
$test->manifest->data->name === 'Remote document') { 'contentType', 'httpLink', 'httpStatus', 'redirectTo'];
return call_user_func('jsonld_default_document_loader', $url); $test_options = (property_exists($this->data, 'option') ?
} $this->data->option : []);
$options = [];
foreach ($test_options as $k => $v) {
if (!in_array($k, $http_options)) {
$options[$k] = $v;
}
}
// attempt to load locally $options['documentLoader'] = $this->createDocumentLoader();
return call_user_func($load_locally, $url); $options = array_merge($options, $opts);
}; if (isset($options['expandContext'])) {
$filename = join(
DIRECTORY_SEPARATOR, [$this->dirname, $options['expandContext']]);
$options['expandContext'] = Util::readJson($filename);
}
return $local_loader; return $options;
} }
public function createDocumentLoader()
{
$base = 'http://json-ld.org/test-suite';
$test = $this;
$load_locally = function($url) use ($test, $base) {
$doc = (object) [
'contextUrl' => null, 'documentUrl' => $url, 'document' => null];
$options = (property_exists($test->data, 'option') ?
$test->data->option : null);
if ($options and $url === $test->base) {
if (property_exists($options, 'redirectTo') &&
property_exists($options, 'httpStatus') &&
$options->httpStatus >= '300'
) {
$doc->documentUrl = ($test->manifest->data->baseIri .
$options->redirectTo);
} else if (property_exists($options, 'httpLink')) {
$content_type = (property_exists($options, 'contentType') ?
$options->contentType : null);
$extension = pathinfo($url, PATHINFO_EXTENSION);
if (!$content_type && $extension === 'jsonld') {
$content_type = 'application/ld+json';
}
$link_header = $options->httpLink;
if (is_array($link_header)) {
$link_header = join(',', $link_header);
}
$link_header = jsonld_parse_link_header($link_header);
if (isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
$link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
} else {
$link_header = null;
}
if ($link_header && $content_type !== 'application/ld+json') {
if (is_array($link_header)) {
throw new Exception('multiple context link headers');
}
$doc->contextUrl = $link_header->target;
}
}
}
global $ROOT_MANIFEST_DIR;
if (strpos($doc->documentUrl, ':') === false) {
$filename = join(
DIRECTORY_SEPARATOR, [
$ROOT_MANIFEST_DIR, $doc->documentUrl]);
$doc->documentUrl = 'file://' . $filename;
} else {
$filename = join(
DIRECTORY_SEPARATOR, [
$ROOT_MANIFEST_DIR, substr($doc->documentUrl, strlen($base))]);
}
try {
$doc->document = Util::readJson($filename);
} catch (Exception $e) {
throw new Exception('loading document failed');
}
return $doc;
};
$local_loader = function($url) use ($test, $base, $load_locally) {
// always load remote-doc and non-base tests remotely
if ((strpos($url, $base) !== 0 && strpos($url, ':') !== false) ||
$test->manifest->data->name === 'Remote document') {
return call_user_func('jsonld_default_document_loader', $url);
}
// attempt to load locally
return call_user_func($load_locally, $url);
};
return $local_loader;
}
public function getJsonLdErrorCode($err)
{
if ($err instanceof JsonLdException) {
if ($err->getCode()) {
return $err->getCode();
}
if ($err->cause) {
return $this->getJsonLdErrorCode($err->cause);
}
}
return $err->getMessage();
}
public function getJsonLdErrorCode($err) {
if($err instanceof JsonLdException) {
if($err->getCode()) {
return $err->getCode();
}
if($err->cause) {
return $this->getJsonLdErrorCode($err->cause);
}
}
return $err->getMessage();
}
} }
class JsonLdTestIterator implements Iterator { class JsonLdTestIterator implements Iterator
/** {
* The current test index. /**
*/ * The current test index.
protected $index = 0; */
protected $index = 0;
/** /**
* The total number of tests. * The total number of tests.
*/ */
protected $count = 0; protected $count = 0;
/** /**
* Creates a TestIterator. * Creates a TestIterator.
* *
* @param string $type the type of tests to iterate over. * @param string $type the type of tests to iterate over.
*/ */
public function __construct($type) { public function __construct($type)
global $TESTS; {
if(isset($TESTS[$type])) { global $TESTS;
$this->tests = $TESTS[$type]; if (isset($TESTS[$type])) {
} else { $this->tests = $TESTS[$type];
$this->tests = array(); } else {
} $this->tests = [];
$this->count = count($this->tests); }
}
/** $this->count = count($this->tests);
* Gets the parameters for the next test. }
*
* @return assoc the parameters for the next test.
*/
public function current() {
return array('test' => $this->tests[$this->index]);
}
/** /**
* Gets the current test number. * Gets the parameters for the next test.
* *
* @return int the current test number. * @return assoc the parameters for the next test.
*/ */
public function key() { public function current()
return $this->index; {
} return ['test' => $this->tests[$this->index]];
}
/** /**
* Proceeds to the next test. * Gets the current test number.
*/ *
public function next() { * @return int the current test number.
$this->index += 1; */
} public function key()
{
return $this->index;
}
/** /**
* Rewinds to the first test. * Proceeds to the next test.
*/ */
public function rewind() { public function next()
$this->index = 0; {
} $this->index += 1;
}
/** /**
* Returns true if there are more tests to be run. * Rewinds to the first test.
* */
* @return bool true if there are more tests to be run. public function rewind()
*/ {
public function valid() { $this->index = 0;
return $this->index < $this->count; }
}
/**
* Returns true if there are more tests to be run.
*
* @return bool true if there are more tests to be run.
*/
public function valid()
{
return $this->index < $this->count;
}
} }
class EarlReport extends PHPUnit_Util_Printer class EarlReport extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener
implements PHPUnit_Framework_TestListener { {
public function __construct() { public function __construct()
$this->filename = null; {
$this->attached = false; $this->filename = null;
$this->report = (object)array( $this->attached = false;
'@context' => (object)array( $this->report = (object) [
'doap' => 'http://usefulinc.com/ns/doap#', '@context' => (object) [
'foaf' => 'http://xmlns.com/foaf/0.1/', 'doap' => 'http://usefulinc.com/ns/doap#',
'dc' => 'http://purl.org/dc/terms/', 'foaf' => 'http://xmlns.com/foaf/0.1/',
'earl' => 'http://www.w3.org/ns/earl#', 'dc' => 'http://purl.org/dc/terms/',
'xsd' => 'http://www.w3.org/2001/XMLSchema#', 'earl' => 'http://www.w3.org/ns/earl#',
'doap:homepage' => (object)array('@type' => '@id'), 'xsd' => 'http://www.w3.org/2001/XMLSchema#',
'doap:license' => (object)array('@type' => '@id'), 'doap:homepage' => (object) ['@type' => '@id'],
'dc:creator' => (object)array('@type' => '@id'), 'doap:license' => (object) ['@type' => '@id'],
'foaf:homepage' => (object)array('@type' => '@id'), 'dc:creator' => (object) ['@type' => '@id'],
'subjectOf' => (object)array('@reverse' => 'earl:subject'), 'foaf:homepage' => (object) ['@type' => '@id'],
'earl:assertedBy' => (object)array('@type' => '@id'), 'subjectOf' => (object) ['@reverse' => 'earl:subject'],
'earl:mode' => (object)array('@type' => '@id'), 'earl:assertedBy' => (object) ['@type' => '@id'],
'earl:test' => (object)array('@type' => '@id'), 'earl:mode' => (object) ['@type' => '@id'],
'earl:outcome' => (object)array('@type' => '@id'), 'earl:test' => (object) ['@type' => '@id'],
'dc:date' => (object)array('@type' => 'xsd:date') 'earl:outcome' => (object) ['@type' => '@id'],
), 'dc:date' => (object) ['@type' => 'xsd:date']
'@id' => 'https://github.com/digitalbazaar/php-json-ld', ],
'@type' => array('doap:Project', 'earl:TestSubject', 'earl:Software'), '@id' => 'https://github.com/digitalbazaar/php-json-ld',
'doap:name' => 'php-json-ld', '@type' => ['doap:Project', 'earl:TestSubject', 'earl:Software'],
'dc:title' => 'php-json-ld', 'doap:name' => 'php-json-ld',
'doap:homepage' => 'https://github.com/digitalbazaar/php-json-ld', 'dc:title' => 'php-json-ld',
'doap:license' => 'https://github.com/digitalbazaar/php-json-ld/blob/master/LICENSE', 'doap:homepage' => 'https://github.com/digitalbazaar/php-json-ld',
'doap:description' => 'A JSON-LD processor for PHP', 'doap:license' => 'https://github.com/digitalbazaar/php-json-ld/blob/master/LICENSE',
'doap:programming-language' => 'PHP', 'doap:description' => 'A JSON-LD processor for PHP',
'dc:creator' => 'https://github.com/dlongley', 'doap:programming-language' => 'PHP',
'doap:developer' => (object)array( 'dc:creator' => 'https://github.com/dlongley',
'@id' => 'https://github.com/dlongley', 'doap:developer' => (object) [
'@type' => array('foaf:Person', 'earl:Assertor'), '@id' => 'https://github.com/dlongley',
'foaf:name' => 'Dave Longley', '@type' => ['foaf:Person', 'earl:Assertor'],
'foaf:homepage' => 'https://github.com/dlongley' 'foaf:name' => 'Dave Longley',
), 'foaf:homepage' => 'https://github.com/dlongley'
'dc:date' => array( ],
'@value' => gmdate('Y-m-d'), 'dc:date' => [
'@type' => 'xsd:date' '@value' => gmdate('Y-m-d'),
), '@type' => 'xsd:date'
'subjectOf' => array() ],
); 'subjectOf' => []
} ];
}
/** /**
* Attaches to the given test result, if not yet attached. * Attaches to the given test result, if not yet attached.
* *
* @param PHPUnit_Framework_Test $result the result to attach to. * @param PHPUnit_Framework_Test $result the result to attach to.
*/ */
public function attach(PHPUnit_Framework_TestResult $result) { public function attach(PHPUnit_Framework_TestResult $result)
if(!$this->attached && $this->filename) { {
$this->attached = true; if (!$this->attached && $this->filename) {
$result->addListener($this); $this->attached = true;
} $result->addListener($this);
} }
}
/** /**
* Adds an assertion to this EARL report. * Adds an assertion to this EARL report.
* *
* @param JsonLdTest $test the JsonLdTest for the assertion is for. * @param JsonLdTest $test the JsonLdTest for the assertion is for.
* @param bool $passed whether or not the test passed. * @param bool $passed whether or not the test passed.
*/ */
public function addAssertion($test, $passed) { public function addAssertion($test, $passed)
$this->report->{'subjectOf'}[] = (object)array( {
'@type' => 'earl:Assertion', $this->report->{'subjectOf'}[] = (object) [
'earl:assertedBy' => $this->report->{'doap:developer'}->{'@id'}, '@type' => 'earl:Assertion',
'earl:mode' => 'earl:automatic', 'earl:assertedBy' => $this->report->{'doap:developer'}->{'@id'},
'earl:test' => $test->data->{'@id'}, 'earl:mode' => 'earl:automatic',
'earl:result' => (object)array( 'earl:test' => $test->data->{'@id'},
'@type' => 'earl:TestResult', 'earl:result' => (object) [
'dc:date' => gmdate(DateTime::ISO8601), '@type' => 'earl:TestResult',
'earl:outcome' => $passed ? 'earl:passed' : 'earl:failed' 'dc:date' => gmdate(DateTime::ISO8601),
) 'earl:outcome' => $passed ? 'earl:passed' : 'earl:failed'
); ]
return $this; ];
}
/** return $this;
* Writes this EARL report to a file. }
*/
public function flush() {
if($this->filename) {
printf("\nWriting EARL report to: %s\n", $this->filename);
$fd = fopen($this->filename, 'w');
fwrite($fd, Util::jsonldEncode($this->report));
fclose($fd);
}
}
public function endTest(PHPUnit_Framework_Test $test, $time) { /**
$this->addAssertion($test->test, true); * Writes this EARL report to a file.
} */
public function flush()
{
if ($this->filename) {
printf("\nWriting EARL report to: %s\n", $this->filename);
$fd = fopen($this->filename, 'w');
fwrite($fd, Util::jsonldEncode($this->report));
fclose($fd);
}
}
public function addError( public function endTest(PHPUnit_Framework_Test $test, $time)
PHPUnit_Framework_Test $test, Exception $e, $time) { {
$this->addAssertion($test->test, false); $this->addAssertion($test->test, true);
} }
public function addFailure( public function addError(
PHPUnit_Framework_Test $test, PHPUnit_Framework_Test $test, Exception $e, $time)
PHPUnit_Framework_AssertionFailedError $e, $time) { {
$this->addAssertion($test->test, false); $this->addAssertion($test->test, false);
if($test->result->shouldStop()) { }
printf("\n\nFAILED\n");
printf("Test: %s\n", $test->test->name);
printf("Purpose: %s\n", $test->test->data->purpose);
printf("EXPECTED: %s\n", Util::jsonldEncode($test->test->expected));
printf("ACTUAL: %s\n", Util::jsonldEncode($test->test->actual));
}
}
public function addIncompleteTest( public function addFailure(
PHPUnit_Framework_Test $test, Exception $e, $time) { PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
$this->addAssertion($test->test, false); {
} $this->addAssertion($test->test, false);
if ($test->result->shouldStop()) {
if (isset($test->test->name)) {
$name = $test->test->name;
} else if (isset($test->test->label)) {
$name = $test->test->label;
} else {
$name = 'UNNAMED';
}
printf("\n\nFAILED\n");
printf("Test: %s\n", $name);
printf("Purpose: %s\n", $test->test->data->purpose);
printf("EXPECTED: %s\n", Util::jsonldEncode($test->test->expected));
printf("ACTUAL: %s\n", Util::jsonldEncode($test->test->actual));
}
}
public function addRiskyTest( public function addIncompleteTest(
PHPUnit_Framework_Test $test, Exception $e, $time) {} PHPUnit_Framework_Test $test, Exception $e, $time)
public function addSkippedTest( {
PHPUnit_Framework_Test $test, Exception $e, $time) {} $this->addAssertion($test->test, false);
public function startTest(PHPUnit_Framework_Test $test) {} }
public function startTestSuite(PHPUnit_Framework_TestSuite $suite) {}
public function endTestSuite(PHPUnit_Framework_TestSuite $suite) {} public function addRiskyTest(
PHPUnit_Framework_Test $test, Exception $e, $time)
{
}
public function addSkippedTest(
PHPUnit_Framework_Test $test, Exception $e, $time)
{
}
public function startTest(PHPUnit_Framework_Test $test)
{
}
public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
{
}
public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
{
}
} }
class Util { class Util
public static function readFile($filename) { {
$rval = @file_get_contents($filename); public static function readFile($filename)
if($rval === false) { {
throw new Exception('File read error: ' . $filename); $rval = @file_get_contents($filename);
} if ($rval === false) {
return $rval; throw new Exception('File read error: ' . $filename);
} }
public static function readJson($filename) { return $rval;
$rval = json_decode(self::readFile($filename)); }
if($rval === null) {
throw new Exception('JSON parse error');
}
return $rval;
}
public static function readNQuads($filename) { public static function readJson($filename)
return self::readFile($filename); {
} $rval = json_decode(self::readFile($filename));
if ($rval === null) {
throw new Exception('JSON parse error');
}
public static function jsonldEncode($input) { return $rval;
// newer PHP has a flag to avoid escaped '/' }
if(defined('JSON_UNESCAPED_SLASHES')) {
$options = JSON_UNESCAPED_SLASHES; public static function readNQuads($filename)
if(defined('JSON_PRETTY_PRINT')) { {
$options |= JSON_PRETTY_PRINT; return self::readFile($filename);
} }
$json = json_encode($input, $options);
} else { public static function jsonldEncode($input)
// use a simple string replacement of '\/' to '/'. {
$json = str_replace('\\/', '/', json_encode($input)); // newer PHP has a flag to avoid escaped '/'
} if (defined('JSON_UNESCAPED_SLASHES')) {
return $json; $options = JSON_UNESCAPED_SLASHES;
} if (defined('JSON_PRETTY_PRINT')) {
$options |= JSON_PRETTY_PRINT;
}
$json = json_encode($input, $options);
} else {
// use a simple string replacement of '\/' to '/'.
$json = str_replace('\\/', '/', json_encode($input));
}
return $json;
}
} }
// tests to skip // tests to skip
$SKIP_TESTS = array(); $SKIP_TESTS = [];
// root manifest directory // root manifest directory
$ROOT_MANIFEST_DIR; $ROOT_MANIFEST_DIR;
// parsed tests; keyed by type // parsed tests; keyed by type
$TESTS = array(); $TESTS = [];
// parsed command line options // parsed command line options
$OPTIONS = array(); $OPTIONS = [];
// parse command line options // parse command line options
global $argv; global $argv;
$args = $argv; $args = $argv;
$total = count($args); $total = count($args);
$start = false; $start = false;
for($i = 0; $i < $total; ++$i) { for ($i = 0; $i < $total; ++$i) {
$arg = $args[$i]; $arg = $args[$i];
if(!$start) { if (!$start) {
if(realpath($arg) === realpath(__FILE__)) { if (realpath($arg) === realpath(__FILE__)) {
$start = true; $start = true;
} }
continue; continue;
} }
if($arg[0] !== '-') { if ($arg[0] !== '-') {
break; break;
} }
$i += 1; $i += 1;
$OPTIONS[$arg] = $args[$i]; $OPTIONS[$arg] = $args[$i];
} }
if(!isset($OPTIONS['-d'])) {
$dvar = 'path to json-ld.org/test-suite'; if (!isset($OPTIONS['-d'])) {
$evar = 'file to write EARL report to'; $dvar = 'path to json-ld.org/test-suite';
echo "php-json-ld Tests\n"; $evar = 'file to write EARL report to';
echo "Usage: phpunit test.php -d <$dvar> [-e <$evar>]\n\n"; echo "php-json-ld Tests\n";
exit(0); echo "Usage: phpunit test.php -d <$dvar> [-e <$evar>]\n\n";
exit(0);
} }
// EARL Report // EARL Report
$EARL = new EarlReport(); $EARL = new EarlReport();
if(isset($OPTIONS['-e'])) { if (isset($OPTIONS['-e'])) {
$EARL->filename = $OPTIONS['-e']; $EARL->filename = $OPTIONS['-e'];
} }
// load root manifest // load root manifest
$ROOT_MANIFEST_DIR = realpath($OPTIONS['-d']); $ROOT_MANIFEST_DIR = realpath($OPTIONS['-d']);
$filename = join( $filename = join(
DIRECTORY_SEPARATOR, array($ROOT_MANIFEST_DIR, 'manifest.jsonld')); DIRECTORY_SEPARATOR, [$ROOT_MANIFEST_DIR, 'manifest.jsonld']);
$root_manifest = Util::readJson($filename); $root_manifest = Util::readJson($filename);
$manifest = new JsonLdManifest($root_manifest, $filename); $manifest = new JsonLdManifest($root_manifest, $filename);
$manifest->load($TESTS); $manifest->load($TESTS);