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
This commit is contained in:
Hypolite Petovan 2018-09-27 19:59:13 -04:00
commit 23cd99e8fb
3 changed files with 6385 additions and 6301 deletions

View file

@ -24,7 +24,7 @@
} }
], ],
"require": { "require": {
"php": ">=5.3.0", "php": ">=5.4.0",
"ext-json": "*" "ext-json": "*"
}, },
"autoload": { "autoload": {

11274
jsonld.php
View file

@ -1,4 +1,5 @@
<?php <?php
/** /**
* PHP implementation of the JSON-LD API. * PHP implementation of the JSON-LD API.
* Version: 0.4.8-dev * Version: 0.4.8-dev
@ -48,9 +49,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 +65,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 +83,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);
} }
/** /**
@ -100,9 +104,10 @@ function jsonld_flatten($input, $ctx, $options=array()) {
* *
* @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);
} }
/** /**
@ -119,16 +124,17 @@ function jsonld_frame($input, $frame, $options=array()) {
* *
* @return the linked JSON-LD output. * @return the linked JSON-LD output.
*/ */
function jsonld_link($input, $ctx, $options) { function jsonld_link($input, $ctx, $options)
// API matches running frame with a wildcard frame and embed: '@link' {
// get arguments // API matches running frame with a wildcard frame and embed: '@link'
$frame = new stdClass(); // get arguments
if($ctx) { $frame = new stdClass();
$frame->{'@context'} = $ctx; if ($ctx) {
} $frame->{'@context'} = $ctx;
$frame->{'@embed'} = '@link'; }
return jsonld_frame($input, $frame, $options); $frame->{'@embed'} = '@link';
}; return jsonld_frame($input, $frame, $options);
}
/** /**
* Performs RDF dataset normalization on the given input. The input is * Performs RDF dataset normalization on the given input. The input is
@ -146,9 +152,10 @@ function jsonld_link($input, $ctx, $options) {
* *
* @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);
} }
/** /**
@ -166,9 +173,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);
} }
/** /**
@ -185,9 +193,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);
} }
/** /**
@ -201,13 +210,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));
} }
/** /**
@ -217,8 +227,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);
} }
/** /**
@ -241,35 +252,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;
} }
/** /**
@ -277,9 +290,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. */
@ -298,9 +312,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;
} }
/** /**
@ -310,19 +325,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;
} }
/** /**
@ -332,76 +349,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;
} }
/** /**
@ -411,85 +432,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. */
@ -505,9 +533,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;
} }
/** /**
@ -515,11 +544,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});
}
} }
/** /**
@ -529,52 +559,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 = '';
}
$keys = array( $keys = [
'href', 'protocol', 'scheme', '?authority', 'authority', 'href', 'protocol', 'scheme', '?authority', 'authority',
'?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path', '?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path',
'?query', 'query', '?fragment', 'fragment'); '?query', 'query', '?fragment', 'fragment'];
$regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/"; $regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/";
preg_match($regex, $url, $match); preg_match($regex, $url, $match);
$rval = array(); $rval = [];
$flags = array(); $flags = [];
$len = count($keys); $len = count($keys);
for($i = 0; $i < $len; ++$i) { for ($i = 0; $i < $len; ++$i) {
$key = $keys[$i]; $key = $keys[$i];
if(strpos($key, '?') === 0) { if (strpos($key, '?') === 0) {
$flags[substr($key, 1)] = !empty($match[$i]); $flags[substr($key, 1)] = !empty($match[$i]);
} else if(!isset($match[$i])) { } else if (!isset($match[$i])) {
$rval[$key] = null; $rval[$key] = null;
} else { } else {
$rval[$key] = $match[$i]; $rval[$key] = $match[$i];
} }
} }
if(!$flags['authority']) { if (!$flags['authority']) {
$rval['authority'] = null; $rval['authority'] = null;
} }
if(!$flags['auth']) { if (!$flags['auth']) {
$rval['auth'] = $rval['user'] = $rval['pass'] = null; $rval['auth'] = $rval['user'] = $rval['pass'] = null;
} }
if(!$flags['port']) { if (!$flags['port']) {
$rval['port'] = null; $rval['port'] = null;
} }
if(!$flags['query']) { if (!$flags['query']) {
$rval['query'] = null; $rval['query'] = null;
} }
if(!$flags['fragment']) { if (!$flags['fragment']) {
$rval['fragment'] = null; $rval['fragment'] = null;
} }
$rval['normalizedPath'] = jsonld_remove_dot_segments( $rval['normalizedPath'] = jsonld_remove_dot_segments(
$rval['path'], !!$rval['authority']); $rval['path'], !!$rval['authority']);
return $rval; return $rval;
} }
/** /**
@ -583,36 +614,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);
} }
/** /**
@ -623,90 +655,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);
// per RFC3986 5.2.2 // per RFC3986 5.2.2
$transform = array('protocol' => $base['protocol']); $transform = ['protocol' => $base['protocol']];
if($rel['authority'] !== null) { if ($rel['authority'] !== null) {
$transform['authority'] = $rel['authority']; $transform['authority'] = $rel['authority'];
$transform['path'] = $rel['path']; $transform['path'] = $rel['path'];
$transform['query'] = $rel['query']; $transform['query'] = $rel['query'];
} else { } else {
$transform['authority'] = $base['authority']; $transform['authority'] = $base['authority'];
if($rel['path'] === '') { if ($rel['path'] === '') {
$transform['path'] = $base['path']; $transform['path'] = $base['path'];
if($rel['query'] !== null) { if ($rel['query'] !== null) {
$transform['query'] = $rel['query']; $transform['query'] = $rel['query'];
} else { } else {
$transform['query'] = $base['query']; $transform['query'] = $base['query'];
} }
} else { } else {
if(strpos($rel['path'], '/') === 0) { if (strpos($rel['path'], '/') === 0) {
// IRI represents an absolute path // IRI represents an absolute path
$transform['path'] = $rel['path']; $transform['path'] = $rel['path'];
} else { } else {
// merge paths // merge paths
$path = $base['path']; $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'];
} }
$transform['path'] = $path; $transform['path'] = $path;
} }
$transform['query'] = $rel['query']; $transform['query'] = $rel['query'];
} }
} }
// remove slashes and dots in path // remove slashes and dots in path
$transform['path'] = jsonld_remove_dot_segments( $transform['path'] = jsonld_remove_dot_segments(
$transform['path'], !!$transform['authority']); $transform['path'], !!$transform['authority']);
// construct URL // construct URL
$rval = $transform['protocol']; $rval = $transform['protocol'];
if($transform['authority'] !== null) { if ($transform['authority'] !== null) {
$rval .= '//' . $transform['authority']; $rval .= '//' . $transform['authority'];
} }
$rval .= $transform['path']; $rval .= $transform['path'];
if($transform['query'] !== null) { if ($transform['query'] !== null) {
$rval .= '?' . $transform['query']; $rval .= '?' . $transform['query'];
} }
if($rel['fragment'] !== null) { if ($rel['fragment'] !== null) {
$rval .= '#' . $rel['fragment']; $rval .= '#' . $rel['fragment'];
} }
// handle empty base // handle empty base
if($rval === '') { if ($rval === '') {
$rval = './'; $rval = './';
} }
return $rval; return $rval;
} }
/** /**
@ -718,5321 +751,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 = ($rel['query'] || $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($rel['query'] !== null) { if ($rel['query'] !== null) {
$rval .= "?{$rel['query']}"; $rval .= "?{$rel['query']}";
} }
if($rel['fragment'] !== null) { 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,
'link' => false)); 'graph' => false,
if($options['link']) { 'skipExpansion' => false,
// force skip expansion when linking, "link" is not part of the 'activeCtx' => false,
// public API, it should only be called from framing 'documentLoader' => $jsonld_default_load_document,
$options['skipExpansion'] = true; 'link' => false]);
} if ($options['link']) {
// force skip expansion when linking, "link" is not part of the
if($options['skipExpansion'] === true) { // public API, it should only be called from framing
$expanded = $input; $options['skipExpansion'] = true;
} else { }
// expand input
try { if ($options['skipExpansion'] === true) {
$expanded = $this->expand($input, $options); $expanded = $input;
} catch(JsonLdException $e) { } else {
throw new JsonLdException( // expand input
'Could not expand input before compaction.', try {
'jsonld.CompactError', null, null, $e); $expanded = $this->expand($input, $options);
} } catch (JsonLdException $e) {
} throw new JsonLdException(
'Could not expand input before compaction.', 'jsonld.CompactError', null, null, $e);
// process context }
$active_ctx = $this->_getInitialContext($options); }
try {
$active_ctx = $this->processContext($active_ctx, $ctx, $options); // process context
} catch(JsonLdException $e) { $active_ctx = $this->_getInitialContext($options);
throw new JsonLdException( try {
'Could not process context before compaction.', $active_ctx = $this->processContext($active_ctx, $ctx, $options);
'jsonld.CompactError', null, null, $e); } catch (JsonLdException $e) {
} throw new JsonLdException(
'Could not process context before compaction.', 'jsonld.CompactError', null, null, $e);
// do compaction }
$compacted = $this->_compact($active_ctx, null, $expanded, $options);
// do compaction
if($options['compactArrays'] && $compacted = $this->_compact($active_ctx, null, $expanded, $options);
!$options['graph'] && is_array($compacted)) {
if(count($compacted) === 1) { if ($options['compactArrays'] &&
// simplify to a single item !$options['graph'] && is_array($compacted)) {
$compacted = $compacted[0]; if (count($compacted) === 1) {
} else if(count($compacted) === 0) { // simplify to a single item
// simplify to an empty object $compacted = $compacted[0];
$compacted = new stdClass(); } else if (count($compacted) === 0) {
} // simplify to an empty object
} else if($options['graph']) { $compacted = new stdClass();
// always use array if graph option is on }
$compacted = self::arrayify($compacted); } else if ($options['graph']) {
} // always use array if graph option is on
$compacted = self::arrayify($compacted);
// follow @context key }
if(is_object($ctx) && property_exists($ctx, '@context')) {
$ctx = $ctx->{'@context'}; // follow @context key
} if (is_object($ctx) && property_exists($ctx, '@context')) {
$ctx = $ctx->{'@context'};
// build output context }
$ctx = self::copy($ctx);
$ctx = self::arrayify($ctx); // build output context
$ctx = self::copy($ctx);
// remove empty contexts $ctx = self::arrayify($ctx);
$tmp = $ctx;
$ctx = array(); // remove empty contexts
foreach($tmp as $v) { $tmp = $ctx;
if(!is_object($v) || count(array_keys((array)$v)) > 0) { $ctx = [];
$ctx[] = $v; foreach ($tmp as $v) {
} if (!is_object($v) || count(array_keys((array) $v)) > 0) {
} $ctx[] = $v;
}
// remove array if only one context }
$ctx_length = count($ctx);
$has_context = ($ctx_length > 0); // remove array if only one context
if($ctx_length === 1) { $ctx_length = count($ctx);
$ctx = $ctx[0]; $has_context = ($ctx_length > 0);
} if ($ctx_length === 1) {
$ctx = $ctx[0];
// add context and/or @graph }
if(is_array($compacted)) {
// use '@graph' keyword // add context and/or @graph
$kwgraph = $this->_compactIri($active_ctx, '@graph'); if (is_array($compacted)) {
$graph = $compacted; // use '@graph' keyword
$compacted = new stdClass(); $kwgraph = $this->_compactIri($active_ctx, '@graph');
if($has_context) { $graph = $compacted;
$compacted->{'@context'} = $ctx; $compacted = new stdClass();
} if ($has_context) {
$compacted->{$kwgraph} = $graph; $compacted->{'@context'} = $ctx;
} else if(is_object($compacted) && $has_context) { }
// reorder keys so @context is first $compacted->{$kwgraph} = $graph;
$graph = $compacted; } else if (is_object($compacted) && $has_context) {
$compacted = new stdClass(); // reorder keys so @context is first
$compacted->{'@context'} = $ctx; $graph = $compacted;
foreach($graph as $k => $v) { $compacted = new stdClass();
$compacted->{$k} = $v; $compacted->{'@context'} = $ctx;
} foreach ($graph as $k => $v) {
} $compacted->{$k} = $v;
}
if($options['activeCtx']) { }
return array('compacted' => $compacted, 'activeCtx' => $active_ctx);
} if ($options['activeCtx']) {
return ['compacted' => $compacted, 'activeCtx' => $active_ctx];
return $compacted; }
}
return $compacted;
/** }
* Performs JSON-LD expansion.
* /**
* @param mixed $input the JSON-LD object to expand. * Performs JSON-LD expansion.
* @param assoc $options the options to use: *
* [base] the base IRI to use. * @param mixed $input the JSON-LD object to expand.
* [expandContext] a context to expand with. * @param assoc $options the options to use:
* [keepFreeFloatingNodes] true to keep free-floating nodes, * [base] the base IRI to use.
* false not to, defaults to false. * [expandContext] a context to expand with.
* [documentLoader(url)] the document loader. * [keepFreeFloatingNodes] true to keep free-floating nodes,
* * false not to, defaults to false.
* @return array the expanded JSON-LD output. * [documentLoader(url)] the document loader.
*/ *
public function expand($input, $options) { * @return array the expanded JSON-LD output.
global $jsonld_default_load_document; */
self::setdefaults($options, array( public function expand($input, $options)
'keepFreeFloatingNodes' => false, {
'documentLoader' => $jsonld_default_load_document)); global $jsonld_default_load_document;
self::setdefaults($options, [
// if input is a string, attempt to dereference remote document 'keepFreeFloatingNodes' => false,
if(is_string($input)) { 'documentLoader' => $jsonld_default_load_document]);
$remote_doc = call_user_func($options['documentLoader'], $input);
} else { // if input is a string, attempt to dereference remote document
$remote_doc = (object)array( if (is_string($input)) {
'contextUrl' => null, $remote_doc = call_user_func($options['documentLoader'], $input);
'documentUrl' => null, } else {
'document' => $input); $remote_doc = (object) [
} 'contextUrl' => null,
'documentUrl' => null,
try { 'document' => $input];
if($remote_doc->document === null) { }
throw new JsonLdException(
'No remote document found at the given URL.', try {
'jsonld.NullRemoteDocument'); if ($remote_doc->document === null) {
} throw new JsonLdException(
if(is_string($remote_doc->document)) { 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument');
$remote_doc->document = self::_parse_json($remote_doc->document); }
} if (is_string($remote_doc->document)) {
} catch(Exception $e) { $remote_doc->document = self::_parse_json($remote_doc->document);
throw new JsonLdException( }
'Could not retrieve a JSON-LD document from the URL.', } catch (Exception $e) {
'jsonld.LoadDocumentError', 'loading document failed', throw new JsonLdException(
array('remoteDoc' => $remote_doc), $e); 'Could not retrieve a JSON-LD document from the URL.', 'jsonld.LoadDocumentError', 'loading document failed', ['remoteDoc' => $remote_doc], $e);
} }
// set default base // set default base
self::setdefault($options, 'base', $remote_doc->documentUrl ?: ''); self::setdefault($options, 'base', $remote_doc->documentUrl ?: '');
// build meta-object and retrieve all @context urls // build meta-object and retrieve all @context urls
$input = (object)array( $input = (object) [
'document' => self::copy($remote_doc->document), 'document' => self::copy($remote_doc->document),
'remoteContext' => (object)array( 'remoteContext' => (object) [
'@context' => $remote_doc->contextUrl)); '@context' => $remote_doc->contextUrl]];
if(isset($options['expandContext'])) { if (isset($options['expandContext'])) {
$expand_context = self::copy($options['expandContext']); $expand_context = self::copy($options['expandContext']);
if(is_object($expand_context) && if (is_object($expand_context) &&
property_exists($expand_context, '@context')) { property_exists($expand_context, '@context')) {
$input->expandContext = $expand_context; $input->expandContext = $expand_context;
} else { } else {
$input->expandContext = (object)array('@context' => $expand_context); $input->expandContext = (object) ['@context' => $expand_context];
} }
} }
// retrieve all @context URLs in the input // retrieve all @context URLs in the input
try { try {
$this->_retrieveContextUrls( $this->_retrieveContextUrls(
$input, new stdClass(), $options['documentLoader'], $options['base']); $input, new stdClass(), $options['documentLoader'], $options['base']);
} catch(Exception $e) { } catch (Exception $e) {
throw new JsonLdException( throw new JsonLdException(
'Could not perform JSON-LD expansion.', 'Could not perform JSON-LD expansion.', 'jsonld.ExpandError', null, null, $e);
'jsonld.ExpandError', null, null, $e); }
}
$active_ctx = $this->_getInitialContext($options);
$active_ctx = $this->_getInitialContext($options); $document = $input->document;
$document = $input->document; $remote_context = $input->remoteContext->{'@context'};
$remote_context = $input->remoteContext->{'@context'};
// process optional expandContext
// process optional expandContext if (property_exists($input, 'expandContext')) {
if(property_exists($input, 'expandContext')) { $active_ctx = self::_processContext(
$active_ctx = self::_processContext( $active_ctx, $input->expandContext, $options);
$active_ctx, $input->expandContext, $options); }
}
// process remote context from HTTP Link Header
// process remote context from HTTP Link Header if ($remote_context) {
if($remote_context) { $active_ctx = self::_processContext(
$active_ctx = self::_processContext( $active_ctx, $remote_context, $options);
$active_ctx, $remote_context, $options); }
}
// do expansion
// do expansion $expanded = $this->_expand($active_ctx, null, $document, $options, false);
$expanded = $this->_expand($active_ctx, null, $document, $options, false);
// optimize away @graph with no other properties
// optimize away @graph with no other properties if (is_object($expanded) && property_exists($expanded, '@graph') &&
if(is_object($expanded) && property_exists($expanded, '@graph') && count(array_keys((array) $expanded)) === 1) {
count(array_keys((array)$expanded)) === 1) { $expanded = $expanded->{'@graph'};
$expanded = $expanded->{'@graph'}; } else if ($expanded === null) {
} else if($expanded === null) { $expanded = [];
$expanded = array(); }
} // normalize to an array
// normalize to an array return self::arrayify($expanded);
return self::arrayify($expanded); }
}
/**
/** * Performs JSON-LD flattening.
* Performs JSON-LD flattening. *
* * @param mixed $input the JSON-LD to flatten.
* @param mixed $input the JSON-LD to flatten. * @param ctx the context to use to compact the flattened output, or null.
* @param ctx the context to use to compact the flattened output, or null. * @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. * [expandContext] a context to expand with.
* [expandContext] a context to expand with. * [documentLoader(url)] the document loader.
* [documentLoader(url)] the document loader. *
* * @return array the flattened output.
* @return array the flattened output. */
*/ public function flatten($input, $ctx, $options)
public function flatten($input, $ctx, $options) { {
global $jsonld_default_load_document; global $jsonld_default_load_document;
self::setdefaults($options, array( self::setdefaults($options, [
'base' => is_string($input) ? $input : '', 'base' => is_string($input) ? $input : '',
'documentLoader' => $jsonld_default_load_document)); 'documentLoader' => $jsonld_default_load_document]);
try { try {
// expand input // expand input
$expanded = $this->expand($input, $options); $expanded = $this->expand($input, $options);
} catch(Exception $e) { } catch (Exception $e) {
throw new JsonLdException( throw new JsonLdException(
'Could not expand input before flattening.', 'Could not expand input before flattening.', 'jsonld.FlattenError', null, null, $e);
'jsonld.FlattenError', null, null, $e); }
}
// do flattening
// do flattening $flattened = $this->_flatten($expanded);
$flattened = $this->_flatten($expanded);
if ($ctx === null) {
if($ctx === null) { return $flattened;
return $flattened; }
}
// compact result (force @graph option to true, skip expansion)
// compact result (force @graph option to true, skip expansion) $options['graph'] = true;
$options['graph'] = true; $options['skipExpansion'] = true;
$options['skipExpansion'] = true; try {
try { $compacted = $this->compact($flattened, $ctx, $options);
$compacted = $this->compact($flattened, $ctx, $options); } catch (Exception $e) {
} catch(Exception $e) { throw new JsonLdException(
throw new JsonLdException( 'Could not compact flattened output.', 'jsonld.FlattenError', null, null, $e);
'Could not compact flattened output.', }
'jsonld.FlattenError', null, null, $e);
} return $compacted;
}
return $compacted;
} /**
* Performs JSON-LD framing.
/** *
* Performs JSON-LD framing. * @param mixed $input the JSON-LD object to frame.
* * @param stdClass $frame the JSON-LD frame to use.
* @param mixed $input the JSON-LD object to frame. * @param $options the framing options.
* @param stdClass $frame the JSON-LD frame to use. * [base] the base IRI to use.
* @param $options the framing options. * [expandContext] a context to expand with.
* [base] the base IRI to use. * [embed] default @embed flag: '@last', '@always', '@never', '@link'
* [expandContext] a context to expand with. * (default: '@last').
* [embed] default @embed flag: '@last', '@always', '@never', '@link' * [explicit] default @explicit flag (default: false).
* (default: '@last'). * [requireAll] default @requireAll flag (default: true).
* [explicit] default @explicit flag (default: false). * [omitDefault] default @omitDefault flag (default: false).
* [requireAll] default @requireAll flag (default: true). * [documentLoader(url)] the document loader.
* [omitDefault] default @omitDefault flag (default: false). *
* [documentLoader(url)] the document loader. * @return stdClass the framed JSON-LD output.
* */
* @return stdClass the framed JSON-LD output. public function frame($input, $frame, $options)
*/ {
public function frame($input, $frame, $options) { global $jsonld_default_load_document;
global $jsonld_default_load_document; self::setdefaults($options, [
self::setdefaults($options, array( 'base' => is_string($input) ? $input : '',
'base' => is_string($input) ? $input : '', 'compactArrays' => true,
'compactArrays' => true, 'embed' => '@last',
'embed' => '@last', 'explicit' => false,
'explicit' => false, 'requireAll' => true,
'requireAll' => true, 'omitDefault' => false,
'omitDefault' => false, 'documentLoader' => $jsonld_default_load_document]);
'documentLoader' => $jsonld_default_load_document));
// if frame is a string, attempt to dereference remote document
// if frame is a string, attempt to dereference remote document if (is_string($frame)) {
if(is_string($frame)) { $remote_frame = call_user_func($options['documentLoader'], $frame);
$remote_frame = call_user_func($options['documentLoader'], $frame); } else {
} else { $remote_frame = (object) [
$remote_frame = (object)array( 'contextUrl' => null,
'contextUrl' => null, 'documentUrl' => null,
'documentUrl' => null, 'document' => $frame];
'document' => $frame); }
}
try {
try { if ($remote_frame->document === null) {
if($remote_frame->document === null) { throw new JsonLdException(
throw new JsonLdException( 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument');
'No remote document found at the given URL.', }
'jsonld.NullRemoteDocument'); if (is_string($remote_frame->document)) {
} $remote_frame->document = self::_parse_json($remote_frame->document);
if(is_string($remote_frame->document)) { }
$remote_frame->document = self::_parse_json($remote_frame->document); } catch (Exception $e) {
} throw new JsonLdException(
} catch(Exception $e) { 'Could not retrieve a JSON-LD document from the URL.', 'jsonld.LoadDocumentError', 'loading document failed', ['remoteDoc' => $remote_frame], $e);
throw new JsonLdException( }
'Could not retrieve a JSON-LD document from the URL.',
'jsonld.LoadDocumentError', 'loading document failed', // preserve frame context
array('remoteDoc' => $remote_frame), $e); $frame = $remote_frame->document;
} if ($frame !== null) {
$ctx = (property_exists($frame, '@context') ?
// preserve frame context $frame->{'@context'} : new stdClass());
$frame = $remote_frame->document; if ($remote_frame->contextUrl !== null) {
if($frame !== null) { if ($ctx !== null) {
$ctx = (property_exists($frame, '@context') ? $ctx = $remote_frame->contextUrl;
$frame->{'@context'} : new stdClass()); } else {
if($remote_frame->contextUrl !== null) { $ctx = self::arrayify($ctx);
if($ctx !== null) { $ctx[] = $remote_frame->contextUrl;
$ctx = $remote_frame->contextUrl; }
} else { $frame->{'@context'} = $ctx;
$ctx = self::arrayify($ctx); }
$ctx[] = $remote_frame->contextUrl; }
}
$frame->{'@context'} = $ctx; try {
} // expand input
} $expanded = $this->expand($input, $options);
} catch (Exception $e) {
try { throw new JsonLdException(
// expand input 'Could not expand input before framing.', 'jsonld.FrameError', null, null, $e);
$expanded = $this->expand($input, $options); }
} catch(Exception $e) {
throw new JsonLdException( try {
'Could not expand input before framing.', // expand frame
'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.', 'jsonld.FrameError', null, null, $e);
$opts['keepFreeFloatingNodes'] = true; }
$expanded_frame = $this->expand($frame, $opts);
} catch(Exception $e) { // do framing
throw new JsonLdException( $framed = $this->_frame($expanded, $expanded_frame, $options);
'Could not expand frame before framing.',
'jsonld.FrameError', null, null, $e); try {
} // compact result (force @graph option to true, skip expansion, check
// for linked embeds)
// do framing $options['graph'] = true;
$framed = $this->_frame($expanded, $expanded_frame, $options); $options['skipExpansion'] = true;
$options['link'] = new ArrayObject();
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.', 'jsonld.FrameError', null, null, $e);
$options['link'] = new ArrayObject(); }
$options['activeCtx'] = true;
$result = $this->compact($framed, $ctx, $options); $compacted = $result['compacted'];
} catch(Exception $e) { $active_ctx = $result['activeCtx'];
throw new JsonLdException(
'Could not compact framed output.', // get graph alias
'jsonld.FrameError', null, null, $e); $graph = $this->_compactIri($active_ctx, '@graph');
} // remove @preserve from results
$options['link'] = new ArrayObject();
$compacted = $result['compacted']; $compacted->{$graph} = $this->_removePreserve(
$active_ctx = $result['activeCtx']; $active_ctx, $compacted->{$graph}, $options);
return $compacted;
// get graph alias }
$graph = $this->_compactIri($active_ctx, '@graph');
// remove @preserve from results /**
$options['link'] = new ArrayObject(); * Performs JSON-LD normalization.
$compacted->{$graph} = $this->_removePreserve( *
$active_ctx, $compacted->{$graph}, $options); * @param mixed $input the JSON-LD object to normalize.
return $compacted; * @param assoc $options the options to use:
} * [base] the base IRI to use.
* [expandContext] a context to expand with.
/** * [inputFormat] the format if input is not JSON-LD:
* Performs JSON-LD normalization. * 'application/nquads' for N-Quads.
* * [format] the format if output is a string:
* @param mixed $input the JSON-LD object to normalize. * 'application/nquads' for N-Quads.
* @param assoc $options the options to use: * [documentLoader(url)] the document loader.
* [base] the base IRI to use. *
* [expandContext] a context to expand with. * @return mixed the normalized output.
* [inputFormat] the format if input is not JSON-LD: */
* 'application/nquads' for N-Quads. public function normalize($input, $options)
* [format] the format if output is a string: {
* 'application/nquads' for N-Quads. global $jsonld_default_load_document;
* [documentLoader(url)] the document loader. self::setdefaults($options, [
* 'base' => is_string($input) ? $input : '',
* @return mixed the normalized output. 'documentLoader' => $jsonld_default_load_document]);
*/
public function normalize($input, $options) { if (isset($options['inputFormat'])) {
global $jsonld_default_load_document; if ($options['inputFormat'] != 'application/nquads') {
self::setdefaults($options, array( throw new JsonLdException(
'base' => is_string($input) ? $input : '', 'Unknown normalization input format.', 'jsonld.NormalizeError');
'documentLoader' => $jsonld_default_load_document)); }
$dataset = $this->parseNQuads($input);
if(isset($options['inputFormat'])) { } else {
if($options['inputFormat'] != 'application/nquads') { try {
throw new JsonLdException( // convert to RDF dataset then do normalization
'Unknown normalization input format.', 'jsonld.NormalizeError'); $opts = $options;
} if (isset($opts['format'])) {
$dataset = $this->parseNQuads($input); unset($opts['format']);
} else { }
try { $opts['produceGeneralizedRdf'] = false;
// convert to RDF dataset then do normalization $dataset = $this->toRDF($input, $opts);
$opts = $options; } catch (Exception $e) {
if(isset($opts['format'])) { throw new JsonLdException(
unset($opts['format']); 'Could not convert input to RDF dataset before normalization.', 'jsonld.NormalizeError', null, null, $e);
} }
$opts['produceGeneralizedRdf'] = false; }
$dataset = $this->toRDF($input, $opts);
} catch(Exception $e) { // do normalization
throw new JsonLdException( return $this->_normalize($dataset, $options);
'Could not convert input to RDF dataset before normalization.', }
'jsonld.NormalizeError', null, null, $e);
} /**
} * Converts an RDF dataset to JSON-LD.
*
// do normalization * @param mixed $dataset a serialized string of RDF in a format specified
return $this->_normalize($dataset, $options); * by the format option or an RDF dataset to convert.
} * @param assoc $options the options to use:
* [format] the format if input is a string:
/** * 'application/nquads' for N-Quads (default).
* Converts an RDF dataset to JSON-LD. * [useRdfType] true to use rdf:type, false to use @type
* * (default: false).
* @param mixed $dataset a serialized string of RDF in a format specified * [useNativeTypes] true to convert XSD types into native types
* by the format option or an RDF dataset to convert. * (boolean, integer, double), false not to (default: false).
* @param assoc $options the options to use: *
* [format] the format if input is a string: * @return array the JSON-LD output.
* 'application/nquads' for N-Quads (default). */
* [useRdfType] true to use rdf:type, false to use @type public function fromRDF($dataset, $options)
* (default: false). {
* [useNativeTypes] true to convert XSD types into native types global $jsonld_rdf_parsers;
* (boolean, integer, double), false not to (default: false).
* self::setdefaults($options, [
* @return array the JSON-LD output. 'useRdfType' => false,
*/ 'useNativeTypes' => false]);
public function fromRDF($dataset, $options) {
global $jsonld_rdf_parsers; if (!isset($options['format']) && is_string($dataset)) {
// set default format to nquads
self::setdefaults($options, array( $options['format'] = 'application/nquads';
'useRdfType' => false, }
'useNativeTypes' => false));
// handle special format
if(!isset($options['format']) && is_string($dataset)) { if (isset($options['format']) && $options['format']) {
// set default format to nquads // supported formats (processor-specific and global)
$options['format'] = 'application/nquads'; if (($this->rdfParsers !== null &&
} !property_exists($this->rdfParsers, $options['format'])) ||
$this->rdfParsers === null &&
// handle special format !property_exists($jsonld_rdf_parsers, $options['format'])) {
if(isset($options['format']) && $options['format']) { throw new JsonLdException(
// supported formats (processor-specific and global) 'Unknown input format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]);
if(($this->rdfParsers !== null && }
!property_exists($this->rdfParsers, $options['format'])) || if ($this->rdfParsers !== null) {
$this->rdfParsers === null && $callable = $this->rdfParsers->{$options['format']};
!property_exists($jsonld_rdf_parsers, $options['format'])) { } else {
throw new JsonLdException( $callable = $jsonld_rdf_parsers->{$options['format']};
'Unknown input format.', }
'jsonld.UnknownFormat', null, array('format' => $options['format'])); $dataset = call_user_func($callable, $dataset);
} }
if($this->rdfParsers !== null) {
$callable = $this->rdfParsers->{$options['format']}; // convert from RDF
} else { return $this->_fromRDF($dataset, $options);
$callable = $jsonld_rdf_parsers->{$options['format']}; }
}
$dataset = call_user_func($callable, $dataset); /**
} * Outputs the RDF dataset found in the given JSON-LD object.
*
// convert from RDF * @param mixed $input the JSON-LD object.
return $this->_fromRDF($dataset, $options); * @param assoc $options the options to use:
} * [base] the base IRI to use.
* [expandContext] a context to expand with.
/** * [format] the format to use to output a string:
* Outputs the RDF dataset found in the given JSON-LD object. * 'application/nquads' for N-Quads.
* * [produceGeneralizedRdf] true to output generalized RDF, false
* @param mixed $input the JSON-LD object. * to produce only standard RDF (default: false).
* @param assoc $options the options to use: * [documentLoader(url)] the document loader.
* [base] the base IRI to use. *
* [expandContext] a context to expand with. * @return mixed the resulting RDF dataset (or a serialization of it).
* [format] the format to use to output a string: */
* 'application/nquads' for N-Quads. public function toRDF($input, $options)
* [produceGeneralizedRdf] true to output generalized RDF, false {
* to produce only standard RDF (default: false). global $jsonld_default_load_document;
* [documentLoader(url)] the document loader. self::setdefaults($options, [
* 'base' => is_string($input) ? $input : '',
* @return mixed the resulting RDF dataset (or a serialization of it). 'produceGeneralizedRdf' => false,
*/ 'documentLoader' => $jsonld_default_load_document]);
public function toRDF($input, $options) {
global $jsonld_default_load_document; try {
self::setdefaults($options, array( // expand input
'base' => is_string($input) ? $input : '', $expanded = $this->expand($input, $options);
'produceGeneralizedRdf' => false, } catch (JsonLdException $e) {
'documentLoader' => $jsonld_default_load_document)); throw new JsonLdException(
'Could not expand input before serialization to RDF.', 'jsonld.RdfError', null, null, $e);
try { }
// expand input
$expanded = $this->expand($input, $options); // create node map for default graph (and any named graphs)
} catch(JsonLdException $e) { $namer = new UniqueNamer('_:b');
throw new JsonLdException( $node_map = (object) ['@default' => new stdClass()];
'Could not expand input before serialization to RDF.', $this->_createNodeMap($expanded, $node_map, '@default', $namer);
'jsonld.RdfError', null, null, $e);
} // output RDF dataset
$dataset = new stdClass();
// create node map for default graph (and any named graphs) $graph_names = array_keys((array) $node_map);
$namer = new UniqueNamer('_:b'); sort($graph_names);
$node_map = (object)array('@default' => new stdClass()); foreach ($graph_names as $graph_name) {
$this->_createNodeMap($expanded, $node_map, '@default', $namer); $graph = $node_map->{$graph_name};
// skip relative IRIs
// output RDF dataset if ($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) {
$dataset = new stdClass(); $dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options);
$graph_names = array_keys((array)$node_map); }
sort($graph_names); }
foreach($graph_names as $graph_name) {
$graph = $node_map->{$graph_name}; $rval = $dataset;
// skip relative IRIs
if($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) { // convert to output format
$dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options); if (isset($options['format']) && $options['format']) {
} // supported formats
} if ($options['format'] === 'application/nquads') {
$rval = self::toNQuads($dataset);
$rval = $dataset; } else {
throw new JsonLdException(
// convert to output format 'Unknown output format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]);
if(isset($options['format']) && $options['format']) { }
// supported formats }
if($options['format'] === 'application/nquads') {
$rval = self::toNQuads($dataset); return $rval;
} else { }
throw new JsonLdException(
'Unknown output format.', 'jsonld.UnknownFormat', /**
null, array('format' => $options['format'])); * Processes a local context, resolving any URLs as necessary, and returns a
} * new active context in its callback.
} *
* @param stdClass $active_ctx the current active context.
return $rval; * @param mixed $local_ctx the local context to process.
} * @param assoc $options the options to use:
* [documentLoader(url)] the document loader.
/** *
* Processes a local context, resolving any URLs as necessary, and returns a * @return stdClass the new active context.
* new active context in its callback. */
* public function processContext($active_ctx, $local_ctx, $options)
* @param stdClass $active_ctx the current active context. {
* @param mixed $local_ctx the local context to process. global $jsonld_default_load_document;
* @param assoc $options the options to use: self::setdefaults($options, [
* [documentLoader(url)] the document loader. 'base' => '',
* 'documentLoader' => $jsonld_default_load_document]);
* @return stdClass the new active context.
*/ // return initial context early for null context
public function processContext($active_ctx, $local_ctx, $options) { if ($local_ctx === null) {
global $jsonld_default_load_document; return $this->_getInitialContext($options);
self::setdefaults($options, array( }
'base' => '',
'documentLoader' => $jsonld_default_load_document)); // retrieve URLs in local_ctx
$local_ctx = self::copy($local_ctx);
// return initial context early for null context if (is_string($local_ctx) or (
if($local_ctx === null) { is_object($local_ctx) && !property_exists($local_ctx, '@context'))) {
return $this->_getInitialContext($options); $local_ctx = (object) ['@context' => $local_ctx];
} }
try {
// retrieve URLs in local_ctx $this->_retrieveContextUrls(
$local_ctx = self::copy($local_ctx); $local_ctx, new stdClass(), $options['documentLoader'], $options['base']);
if(is_string($local_ctx) or ( } catch (Exception $e) {
is_object($local_ctx) && !property_exists($local_ctx, '@context'))) { throw new JsonLdException(
$local_ctx = (object)array('@context' => $local_ctx); 'Could not process JSON-LD context.', 'jsonld.ContextError', null, null, $e);
} }
try {
$this->_retrieveContextUrls( // process context
$local_ctx, new stdClass(), return $this->_processContext($active_ctx, $local_ctx, $options);
$options['documentLoader'], $options['base']); }
} catch(Exception $e) {
throw new JsonLdException( /**
'Could not process JSON-LD context.', * Returns true if the given subject has the given property.
'jsonld.ContextError', null, null, $e); *
} * @param stdClass $subject the subject to check.
* @param string $property the property to look for.
// process context *
return $this->_processContext($active_ctx, $local_ctx, $options); * @return bool true if the subject has the given property, false if not.
} */
public static function hasProperty($subject, $property)
/** {
* Returns true if the given subject has the given property. $rval = false;
* if (property_exists($subject, $property)) {
* @param stdClass $subject the subject to check. $value = $subject->{$property};
* @param string $property the property to look for. $rval = (!is_array($value) || count($value) > 0);
* }
* @return bool true if the subject has the given property, false if not. return $rval;
*/ }
public static function hasProperty($subject, $property) {
$rval = false; /**
if(property_exists($subject, $property)) { * Determines if the given value is a property of the given subject.
$value = $subject->{$property}; *
$rval = (!is_array($value) || count($value) > 0); * @param stdClass $subject the subject to check.
} * @param string $property the property to check.
return $rval; * @param mixed $value the value to check.
} *
* @return bool true if the value exists, false if not.
/** */
* Determines if the given value is a property of the given subject. public static function hasValue($subject, $property, $value)
* {
* @param stdClass $subject the subject to check. $rval = false;
* @param string $property the property to check. if (self::hasProperty($subject, $property)) {
* @param mixed $value the value to check. $val = $subject->{$property};
* $is_list = self::_isList($val);
* @return bool true if the value exists, false if not. if (is_array($val) || $is_list) {
*/ if ($is_list) {
public static function hasValue($subject, $property, $value) { $val = $val->{'@list'};
$rval = false; }
if(self::hasProperty($subject, $property)) { foreach ($val as $v) {
$val = $subject->{$property}; if (self::compareValues($value, $v)) {
$is_list = self::_isList($val); $rval = true;
if(is_array($val) || $is_list) { break;
if($is_list) { }
$val = $val->{'@list'}; }
} } else if (!is_array($value)) {
foreach($val as $v) { // avoid matching the set of values with an array value parameter
if(self::compareValues($value, $v)) { $rval = self::compareValues($value, $val);
$rval = true; }
break; }
} return $rval;
} }
} else if(!is_array($value)) {
// avoid matching the set of values with an array value parameter /**
$rval = self::compareValues($value, $val); * Adds a value to a subject. If the value is an array, all values in the
} * array will be added.
} *
return $rval; * Note: If the value is a subject that already exists as a property of the
} * given subject, this method makes no attempt to deeply merge properties.
* Instead, the value will not be added.
/** *
* Adds a value to a subject. If the value is an array, all values in the * @param stdClass $subject the subject to add the value to.
* array will be added. * @param string $property the property that relates the value to the subject.
* * @param mixed $value the value to add.
* Note: If the value is a subject that already exists as a property of the * @param assoc [$options] the options to use:
* given subject, this method makes no attempt to deeply merge properties. * [propertyIsArray] true if the property is always an array, false
* Instead, the value will not be added. * if not (default: false).
* * [allowDuplicate] true to allow duplicates, false not to (uses a
* @param stdClass $subject the subject to add the value to. * simple shallow comparison of subject ID or value)
* @param string $property the property that relates the value to the subject. * (default: true).
* @param mixed $value the value to add. */
* @param assoc [$options] the options to use: public static function addValue(
* [propertyIsArray] true if the property is always an array, false $subject, $property, $value, $options = [])
* if not (default: false). {
* [allowDuplicate] true to allow duplicates, false not to (uses a self::setdefaults($options, [
* simple shallow comparison of subject ID or value) 'allowDuplicate' => true,
* (default: true). 'propertyIsArray' => false]);
*/
public static function addValue( if (is_array($value)) {
$subject, $property, $value, $options=array()) { if (count($value) === 0 && $options['propertyIsArray'] &&
self::setdefaults($options, array( !property_exists($subject, $property)) {
'allowDuplicate' => true, $subject->{$property} = [];
'propertyIsArray' => false)); }
foreach ($value as $v) {
if(is_array($value)) { self::addValue($subject, $property, $v, $options);
if(count($value) === 0 && $options['propertyIsArray'] && }
!property_exists($subject, $property)) { } else if (property_exists($subject, $property)) {
$subject->{$property} = array(); // check if subject already has value if duplicates not allowed
} $has_value = (!$options['allowDuplicate'] &&
foreach($value as $v) { self::hasValue($subject, $property, $value));
self::addValue($subject, $property, $v, $options);
} // make property an array if value not present or always an array
} else if(property_exists($subject, $property)) { if (!is_array($subject->{$property}) &&
// check if subject already has value if duplicates not allowed (!$has_value || $options['propertyIsArray'])) {
$has_value = (!$options['allowDuplicate'] && $subject->{$property} = [$subject->{$property}];
self::hasValue($subject, $property, $value)); }
// make property an array if value not present or always an array // add new value
if(!is_array($subject->{$property}) && if (!$has_value) {
(!$has_value || $options['propertyIsArray'])) { $subject->{$property}[] = $value;
$subject->{$property} = array($subject->{$property}); }
} } else {
// add new value as set or single value
// add new value $subject->{$property} = ($options['propertyIsArray'] ?
if(!$has_value) { [$value] : $value);
$subject->{$property}[] = $value; }
} }
} else {
// add new value as set or single value /**
$subject->{$property} = ($options['propertyIsArray'] ? * Gets all of the values for a subject's property as an array.
array($value) : $value); *
} * @param stdClass $subject the subject.
} * @param string $property the property.
*
/** * @return array all of the values for a subject's property as an array.
* Gets all of the values for a subject's property as an array. */
* public static function getValues($subject, $property)
* @param stdClass $subject the subject. {
* @param string $property the property. $rval = (property_exists($subject, $property) ?
* $subject->{$property} : []);
* @return array all of the values for a subject's property as an array. return self::arrayify($rval);
*/ }
public static function getValues($subject, $property) {
$rval = (property_exists($subject, $property) ? /**
$subject->{$property} : array()); * Removes a property from a subject.
return self::arrayify($rval); *
} * @param stdClass $subject the subject.
* @param string $property the property.
/** */
* Removes a property from a subject. public static function removeProperty($subject, $property)
* {
* @param stdClass $subject the subject. unset($subject->{$property});
* @param string $property the property. }
*/
public static function removeProperty($subject, $property) { /**
unset($subject->{$property}); * Removes a value from a subject.
} *
* @param stdClass $subject the subject.
/** * @param string $property the property that relates the value to the subject.
* Removes a value from a subject. * @param mixed $value the value to remove.
* * @param assoc [$options] the options to use:
* @param stdClass $subject the subject. * [propertyIsArray] true if the property is always an array,
* @param string $property the property that relates the value to the subject. * false if not (default: false).
* @param mixed $value the value to remove. */
* @param assoc [$options] the options to use: public static function removeValue(
* [propertyIsArray] true if the property is always an array, $subject, $property, $value, $options = [])
* false if not (default: false). {
*/ self::setdefaults($options, [
public static function removeValue( 'propertyIsArray' => false]);
$subject, $property, $value, $options=array()) {
self::setdefaults($options, array( // filter out value
'propertyIsArray' => false)); $filter = function($e) use ($value) {
return !self::compareValues($e, $value);
// filter out value };
$filter = function($e) use ($value) { $values = self::getValues($subject, $property);
return !self::compareValues($e, $value); $values = array_values(array_filter($values, $filter));
};
$values = self::getValues($subject, $property); if (count($values) === 0) {
$values = array_values(array_filter($values, $filter)); self::removeProperty($subject, $property);
} else if (count($values) === 1 && !$options['propertyIsArray']) {
if(count($values) === 0) { $subject->{$property} = $values[0];
self::removeProperty($subject, $property); } else {
} else if(count($values) === 1 && !$options['propertyIsArray']) { $subject->{$property} = $values;
$subject->{$property} = $values[0]; }
} else { }
$subject->{$property} = $values;
} /**
} * Compares two JSON-LD values for equality. Two JSON-LD values will be
* considered equal if:
/** *
* Compares two JSON-LD values for equality. Two JSON-LD values will be * 1. They are both primitives of the same type and value.
* considered equal if: * 2. They are both @values with the same @value, @type, @language,
* * and @index, OR
* 1. They are both primitives of the same type and value. * 3. They both have @ids that are the same.
* 2. They are both @values with the same @value, @type, @language, *
* and @index, OR * @param mixed $v1 the first value.
* 3. They both have @ids that are the same. * @param mixed $v2 the second value.
* *
* @param mixed $v1 the first value. * @return bool true if v1 and v2 are considered equal, false if not.
* @param mixed $v2 the second value. */
* public static function compareValues($v1, $v2)
* @return bool true if v1 and v2 are considered equal, false if not. {
*/ // 1. equal primitives
public static function compareValues($v1, $v2) { if ($v1 === $v2) {
// 1. equal primitives return true;
if($v1 === $v2) { }
return true;
} // 2. equal @values
if (self::_isValue($v1) && self::_isValue($v2)) {
// 2. equal @values return (
if(self::_isValue($v1) && self::_isValue($v2)) { self::_compareKeyValues($v1, $v2, '@value') &&
return ( self::_compareKeyValues($v1, $v2, '@type') &&
self::_compareKeyValues($v1, $v2, '@value') && self::_compareKeyValues($v1, $v2, '@language') &&
self::_compareKeyValues($v1, $v2, '@type') && self::_compareKeyValues($v1, $v2, '@index'));
self::_compareKeyValues($v1, $v2, '@language') && }
self::_compareKeyValues($v1, $v2, '@index'));
} // 3. equal @ids
if (is_object($v1) && property_exists($v1, '@id') &&
// 3. equal @ids is_object($v2) && property_exists($v2, '@id')) {
if(is_object($v1) && property_exists($v1, '@id') && return $v1->{'@id'} === $v2->{'@id'};
is_object($v2) && property_exists($v2, '@id')) { }
return $v1->{'@id'} === $v2->{'@id'};
} return false;
}
return false;
} /**
* Gets the value for the given active context key and type, null if none is
/** * set.
* Gets the value for the given active context key and type, null if none is *
* set. * @param stdClass $ctx the active context.
* * @param string $key the context key.
* @param stdClass $ctx the active context. * @param string [$type] the type of value to get (eg: '@id', '@type'), if not
* @param string $key the context key. * specified gets the entire entry for a key, null if not found.
* @param string [$type] the type of value to get (eg: '@id', '@type'), if not *
* specified gets the entire entry for a key, null if not found. * @return mixed the value.
* */
* @return mixed the value. public static function getContextValue($ctx, $key, $type)
*/ {
public static function getContextValue($ctx, $key, $type) { $rval = null;
$rval = null;
// return null for invalid key
// return null for invalid key if ($key === null) {
if($key === null) { return $rval;
return $rval; }
}
// get default language
// get default language if ($type === '@language' && property_exists($ctx, $type)) {
if($type === '@language' && property_exists($ctx, $type)) { $rval = $ctx->{$type};
$rval = $ctx->{$type}; }
}
// get specific entry information
// get specific entry information if (property_exists($ctx->mappings, $key)) {
if(property_exists($ctx->mappings, $key)) { $entry = $ctx->mappings->{$key};
$entry = $ctx->mappings->{$key}; if ($entry === null) {
if($entry === null) { return null;
return null; }
}
if ($type === null) {
if($type === null) { // return whole entry
// return whole entry $rval = $entry;
$rval = $entry; } else if (property_exists($entry, $type)) {
} else if(property_exists($entry, $type)) { // return entry value for type
// return entry value for type $rval = $entry->{$type};
$rval = $entry->{$type}; }
} }
}
return $rval;
return $rval; }
}
/**
/** * Parses RDF in the form of N-Quads.
* Parses RDF in the form of N-Quads. *
* * @param string $input the N-Quads input to parse.
* @param string $input the N-Quads input to parse. *
* * @return stdClass an RDF dataset.
* @return stdClass an RDF dataset. */
*/ public static function parseNQuads($input)
public static function parseNQuads($input) { {
// define partial regexes // define partial regexes
$iri = '(?:<([^:]+:[^>]*)>)'; $iri = '(?:<([^:]+:[^>]*)>)';
$bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))'; $bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))';
$plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"'; $plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"';
$datatype = "(?:\\^\\^$iri)"; $datatype = "(?:\\^\\^$iri)";
$language = '(?:@([a-z]+(?:-[a-z0-9]+)*))'; $language = '(?:@([a-z]+(?:-[a-z0-9]+)*))';
$literal = "(?:$plain(?:$datatype|$language)?)"; $literal = "(?:$plain(?:$datatype|$language)?)";
$ws = '[ \\t]'; $ws = '[ \\t]';
$eoln = '/(?:\r\n)|(?:\n)|(?:\r)/'; $eoln = '/(?:\r\n)|(?:\n)|(?:\r)/';
$empty = "/^$ws*$/"; $empty = "/^$ws*$/";
// define quad part regexes // define quad part regexes
$subject = "(?:$iri|$bnode)$ws+"; $subject = "(?:$iri|$bnode)$ws+";
$property = "$iri$ws+"; $property = "$iri$ws+";
$object = "(?:$iri|$bnode|$literal)$ws*"; $object = "(?:$iri|$bnode|$literal)$ws*";
$graph_name = "(?:\\.|(?:(?:$iri|$bnode)$ws*\\.))"; $graph_name = "(?:\\.|(?:(?:$iri|$bnode)$ws*\\.))";
// full quad regex // full quad regex
$quad = "/^$ws*$subject$property$object$graph_name$ws*$/"; $quad = "/^$ws*$subject$property$object$graph_name$ws*$/";
// build RDF dataset // build RDF dataset
$dataset = new stdClass(); $dataset = new stdClass();
// split N-Quad input into lines // split N-Quad input into lines
$lines = preg_split($eoln, $input); $lines = preg_split($eoln, $input);
$line_number = 0; $line_number = 0;
foreach($lines as $line) { foreach ($lines as $line) {
$line_number += 1; $line_number += 1;
// skip empty lines // skip empty lines
if(preg_match($empty, $line)) { if (preg_match($empty, $line)) {
continue; continue;
} }
// parse quad // parse quad
if(!preg_match($quad, $line, $match)) { if (!preg_match($quad, $line, $match)) {
throw new JsonLdException( throw new JsonLdException(
'Error while parsing N-Quads; invalid quad.', 'Error while parsing N-Quads; invalid quad.', 'jsonld.ParseError', null, ['line' => $line_number]);
'jsonld.ParseError', null, array('line' => $line_number)); }
}
// create RDF triple
// create RDF triple $triple = (object) [
$triple = (object)array( 'subject' => new stdClass(),
'subject' => new stdClass(), 'predicate' => new stdClass(),
'predicate' => new stdClass(), 'object' => new stdClass()];
'object' => new stdClass());
// get subject
// get subject if ($match[1] !== '') {
if($match[1] !== '') { $triple->subject->type = 'IRI';
$triple->subject->type = 'IRI'; $triple->subject->value = $match[1];
$triple->subject->value = $match[1]; } else {
} else { $triple->subject->type = 'blank node';
$triple->subject->type = 'blank node'; $triple->subject->value = $match[2];
$triple->subject->value = $match[2]; }
}
// get predicate
// get predicate $triple->predicate->type = 'IRI';
$triple->predicate->type = 'IRI'; $triple->predicate->value = $match[3];
$triple->predicate->value = $match[3];
// get object
// get object if ($match[4] !== '') {
if($match[4] !== '') { $triple->object->type = 'IRI';
$triple->object->type = 'IRI'; $triple->object->value = $match[4];
$triple->object->value = $match[4]; } else if ($match[5] !== '') {
} else if($match[5] !== '') { $triple->object->type = 'blank node';
$triple->object->type = 'blank node'; $triple->object->value = $match[5];
$triple->object->value = $match[5]; } else {
} else { $triple->object->type = 'literal';
$triple->object->type = 'literal'; $unescaped = str_replace(
$unescaped = str_replace( ['\"', '\t', '\n', '\r', '\\\\'], ['"', "\t", "\n", "\r", '\\'], $match[6]);
array('\"', '\t', '\n', '\r', '\\\\'), if (isset($match[7]) && $match[7] !== '') {
array('"', "\t", "\n", "\r", '\\'), $triple->object->datatype = $match[7];
$match[6]); } else if (isset($match[8]) && $match[8] !== '') {
if(isset($match[7]) && $match[7] !== '') { $triple->object->datatype = self::RDF_LANGSTRING;
$triple->object->datatype = $match[7]; $triple->object->language = $match[8];
} else if(isset($match[8]) && $match[8] !== '') { } else {
$triple->object->datatype = self::RDF_LANGSTRING; $triple->object->datatype = self::XSD_STRING;
$triple->object->language = $match[8]; }
} else { $triple->object->value = $unescaped;
$triple->object->datatype = self::XSD_STRING; }
}
$triple->object->value = $unescaped; // get graph name ('@default' is used for the default graph)
} $name = '@default';
if (isset($match[9]) && $match[9] !== '') {
// get graph name ('@default' is used for the default graph) $name = $match[9];
$name = '@default'; } else if (isset($match[10]) && $match[10] !== '') {
if(isset($match[9]) && $match[9] !== '') { $name = $match[10];
$name = $match[9]; }
} else if(isset($match[10]) && $match[10] !== '') {
$name = $match[10]; // initialize graph in dataset
} if (!property_exists($dataset, $name)) {
$dataset->{$name} = [$triple];
// initialize graph in dataset } else {
if(!property_exists($dataset, $name)) { // add triple if unique to its graph
$dataset->{$name} = array($triple); $unique = true;
} else { $triples = &$dataset->{$name};
// add triple if unique to its graph foreach ($triples as $t) {
$unique = true; if (self::_compareRDFTriples($t, $triple)) {
$triples = &$dataset->{$name}; $unique = false;
foreach($triples as $t) { break;
if(self::_compareRDFTriples($t, $triple)) { }
$unique = false; }
break; if ($unique) {
} $triples[] = $triple;
} }
if($unique) { }
$triples[] = $triple; }
}
} return $dataset;
} }
return $dataset; /**
} * Converts an RDF dataset to N-Quads.
*
/** * @param stdClass $dataset the RDF dataset to convert.
* Converts an RDF dataset to N-Quads. *
* * @return string the N-Quads string.
* @param stdClass $dataset the RDF dataset to convert. */
* public static function toNQuads($dataset)
* @return string the N-Quads string. {
*/ $quads = [];
public static function toNQuads($dataset) { foreach ($dataset as $graph_name => $triples) {
$quads = array(); foreach ($triples as $triple) {
foreach($dataset as $graph_name => $triples) { if ($graph_name === '@default') {
foreach($triples as $triple) { $graph_name = null;
if($graph_name === '@default') { }
$graph_name = null; $quads[] = self::toNQuad($triple, $graph_name);
} }
$quads[] = self::toNQuad($triple, $graph_name); }
} sort($quads);
} return implode($quads);
sort($quads); }
return implode($quads);
} /**
* Converts an RDF triple and graph name to an N-Quad string (a single quad).
/** *
* Converts an RDF triple and graph name to an N-Quad string (a single quad). * @param stdClass $triple the RDF triple to convert.
* * @param mixed $graph_name the name of the graph containing the triple, null
* @param stdClass $triple the RDF triple to convert. * for the default graph.
* @param mixed $graph_name the name of the graph containing the triple, null * @param string $bnode the bnode the quad is mapped to (optional, for
* for the default graph. * use during normalization only).
* @param string $bnode the bnode the quad is mapped to (optional, for *
* use during normalization only). * @return string the N-Quad string.
* */
* @return string the N-Quad string. public static function toNQuad($triple, $graph_name, $bnode = null)
*/ {
public static function toNQuad($triple, $graph_name, $bnode=null) { $s = $triple->subject;
$s = $triple->subject; $p = $triple->predicate;
$p = $triple->predicate; $o = $triple->object;
$o = $triple->object; $g = $graph_name;
$g = $graph_name;
$quad = '';
$quad = '';
// subject is an IRI
// subject is an IRI if ($s->type === 'IRI') {
if($s->type === 'IRI') { $quad .= "<{$s->value}>";
$quad .= "<{$s->value}>"; } else if ($bnode !== null) {
} else if($bnode !== null) { // bnode normalization mode
// bnode normalization mode $quad .= ($s->value === $bnode) ? '_:a' : '_:z';
$quad .= ($s->value === $bnode) ? '_:a' : '_:z'; } else {
} else { // bnode normal mode
// bnode normal mode $quad .= $s->value;
$quad .= $s->value; }
} $quad .= ' ';
$quad .= ' ';
// predicate is an IRI
// predicate is an IRI if ($p->type === 'IRI') {
if($p->type === 'IRI') { $quad .= "<{$p->value}>";
$quad .= "<{$p->value}>"; } else if ($bnode !== null) {
} else if($bnode !== null) { // FIXME: TBD what to do with bnode predicates during normalization
// FIXME: TBD what to do with bnode predicates during normalization // bnode normalization mode
// bnode normalization mode $quad .= '_:p';
$quad .= '_:p'; } else {
} else { // bnode normal mode
// bnode normal mode $quad .= $p->value;
$quad .= $p->value; }
} $quad .= ' ';
$quad .= ' ';
// object is IRI, bnode, or literal
// object is IRI, bnode, or literal if ($o->type === 'IRI') {
if($o->type === 'IRI') { $quad .= "<{$o->value}>";
$quad .= "<{$o->value}>"; } else if ($o->type === 'blank node') {
} else if($o->type === 'blank node') { if ($bnode !== null) {
if($bnode !== null) { // normalization mode
// normalization mode $quad .= ($o->value === $bnode) ? '_:a' : '_:z';
$quad .= ($o->value === $bnode) ? '_:a' : '_:z'; } else {
} else { // normal mode
// normal mode $quad .= $o->value;
$quad .= $o->value; }
} } else {
} else { $escaped = str_replace(
$escaped = str_replace( ['\\', "\t", "\n", "\r", '"'], ['\\\\', '\t', '\n', '\r', '\"'], $o->value);
array('\\', "\t", "\n", "\r", '"'), $quad .= '"' . $escaped . '"';
array('\\\\', '\t', '\n', '\r', '\"'), if ($o->datatype === self::RDF_LANGSTRING) {
$o->value); if ($o->language) {
$quad .= '"' . $escaped . '"'; $quad .= "@{$o->language}";
if($o->datatype === self::RDF_LANGSTRING) { }
if($o->language) { } else if ($o->datatype !== self::XSD_STRING) {
$quad .= "@{$o->language}"; $quad .= "^^<{$o->datatype}>";
} }
} else if($o->datatype !== self::XSD_STRING) { }
$quad .= "^^<{$o->datatype}>";
} // graph
} if ($g !== null) {
if (strpos($g, '_:') !== 0) {
// graph $quad .= " <$g>";
if($g !== null) { } else if ($bnode) {
if(strpos($g, '_:') !== 0) { $quad .= ' _:g';
$quad .= " <$g>"; } else {
} else if($bnode) { $quad .= " $g";
$quad .= ' _:g'; }
} else { }
$quad .= " $g";
} $quad .= " .\n";
} return $quad;
}
$quad .= " .\n";
return $quad; /**
} * Registers a processor-specific RDF dataset parser by content-type.
* Global parsers will no longer be used by this processor.
/** *
* Registers a processor-specific RDF dataset parser by content-type. * @param string $content_type the content-type for the parser.
* Global parsers will no longer be used by this processor. * @param callable $parser(input) the parser function (takes a string as
* * a parameter and returns an RDF dataset).
* @param string $content_type the content-type for the parser. */
* @param callable $parser(input) the parser function (takes a string as public function registerRDFParser($content_type, $parser)
* a parameter and returns an RDF dataset). {
*/ if ($this->rdfParsers === null) {
public function registerRDFParser($content_type, $parser) { $this->rdfParsers = new stdClass();
if($this->rdfParsers === null) { }
$this->rdfParsers = new stdClass(); $this->rdfParsers->{$content_type} = $parser;
} }
$this->rdfParsers->{$content_type} = $parser;
} /**
* Unregisters a process-specific RDF dataset parser by content-type. If
/** * there are no remaining processor-specific parsers, then the global
* Unregisters a process-specific RDF dataset parser by content-type. If * parsers will be re-enabled.
* 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.
* */
* @param string $content_type the content-type for the parser. public function unregisterRDFParser($content_type)
*/ {
public function unregisterRDFParser($content_type) { if ($this->rdfParsers !== null &&
if($this->rdfParsers !== null && property_exists($this->rdfParsers, $content_type)) {
property_exists($this->rdfParsers, $content_type)) { unset($this->rdfParsers->{$content_type});
unset($this->rdfParsers->{$content_type}); if (count(get_object_vars($content_type)) === 0) {
if(count(get_object_vars($content_type)) === 0) { $this->rdfParsers = null;
$this->rdfParsers = null; }
} }
} }
}
/**
/** * If $value is an array, returns $value, otherwise returns an array
* If $value is an array, returns $value, otherwise returns an array * containing $value as the only element.
* containing $value as the only element. *
* * @param mixed $value the value.
* @param mixed $value the value. *
* * @return array an array.
* @return array an array. */
*/ public static function arrayify($value)
public static function arrayify($value) { {
return is_array($value) ? $value : array($value); return is_array($value) ? $value : [$value];
} }
/** /**
* Clones an object, array, or string/number. * Clones an object, array, or string/number.
* *
* @param mixed $value the value to clone. * @param mixed $value the value to clone.
* *
* @return mixed the cloned value. * @return mixed the cloned value.
*/ */
public static function copy($value) { public static function copy($value)
if(is_object($value) || is_array($value)) { {
return unserialize(serialize($value)); if (is_object($value) || is_array($value)) {
} return unserialize(serialize($value));
return $value; }
} return $value;
}
/**
* Sets the value of a key for the given array if that property /**
* has not already been set. * Sets the value of a key for the given array if that property
* * has not already been set.
* @param &assoc $arr the object to update. *
* @param string $key the key to update. * @param &assoc $arr the object to update.
* @param mixed $value the value to set. * @param string $key the key to update.
*/ * @param mixed $value the value to set.
public static function setdefault(&$arr, $key, $value) { */
isset($arr[$key]) or $arr[$key] = $value; public static function setdefault(&$arr, $key, $value)
} {
isset($arr[$key]) or $arr[$key] = $value;
/** }
* Sets default values for keys in the given array.
* /**
* @param &assoc $arr the object to update. * Sets default values for keys in the given array.
* @param assoc $defaults the default keys and values. *
*/ * @param &assoc $arr the object to update.
public static function setdefaults(&$arr, $defaults) { * @param assoc $defaults the default keys and values.
foreach($defaults as $key => $value) { */
self::setdefault($arr, $key, $value); public static function setdefaults(&$arr, $defaults)
} {
} foreach ($defaults as $key => $value) {
self::setdefault($arr, $key, $value);
/** }
* Recursively compacts an element using the given active context. All values }
* must be in expanded form before this method is called.
* /**
* @param stdClass $active_ctx the active context to use. * Recursively compacts an element using the given active context. All values
* @param mixed $active_property the compacted property with the element * must be in expanded form before this method is called.
* to compact, null for none. *
* @param mixed $element the element to compact. * @param stdClass $active_ctx the active context to use.
* @param assoc $options the compaction options. * @param mixed $active_property the compacted property with the element
* * to compact, null for none.
* @return mixed the compacted value. * @param mixed $element the element to compact.
*/ * @param assoc $options the compaction options.
protected function _compact( *
$active_ctx, $active_property, $element, $options) { * @return mixed the compacted value.
// recursively compact array */
if(is_array($element)) { protected function _compact(
$rval = array(); $active_ctx, $active_property, $element, $options)
foreach($element as $e) { {
// compact, dropping any null values // recursively compact array
$compacted = $this->_compact( if (is_array($element)) {
$active_ctx, $active_property, $e, $options); $rval = [];
if($compacted !== null) { foreach ($element as $e) {
$rval[] = $compacted; // compact, dropping any null values
} $compacted = $this->_compact(
} $active_ctx, $active_property, $e, $options);
if($options['compactArrays'] && count($rval) === 1) { if ($compacted !== null) {
// use single element if no container is specified $rval[] = $compacted;
$container = self::getContextValue( }
$active_ctx, $active_property, '@container'); }
if($container === null) { if ($options['compactArrays'] && count($rval) === 1) {
$rval = $rval[0]; // use single element if no container is specified
} $container = self::getContextValue(
} $active_ctx, $active_property, '@container');
return $rval; if ($container === null) {
} $rval = $rval[0];
}
// recursively compact object }
if(is_object($element)) { return $rval;
if($options['link'] && property_exists($element, '@id') && }
isset($options['link'][$element->{'@id'}])) {
// check for a linked element to reuse // recursively compact object
$linked = $options['link'][$element->{'@id'}]; if (is_object($element)) {
foreach($linked as $link) { if ($options['link'] && property_exists($element, '@id') &&
if($link['expanded'] === $element) { isset($options['link'][$element->{'@id'}])) {
return $link['compacted']; // check for a linked element to reuse
} $linked = $options['link'][$element->{'@id'}];
} foreach ($linked as $link) {
} if ($link['expanded'] === $element) {
return $link['compacted'];
// do value compaction on @values and subject references }
if(self::_isValue($element) || self::_isSubjectReference($element)) { }
$rval = $this->_compactValue($active_ctx, $active_property, $element); }
if($options['link'] && self::_isSubjectReference($element)) {
// store linked element // do value compaction on @values and subject references
if(!isset($options['link'][$element->{'@id'}])) { if (self::_isValue($element) || self::_isSubjectReference($element)) {
$options['link'][$element->{'@id'}] = array(); $rval = $this->_compactValue($active_ctx, $active_property, $element);
} if ($options['link'] && self::_isSubjectReference($element)) {
$options['link'][$element->{'@id'}][] = array( // store linked element
'expanded' => $element, 'compacted' => $rval); if (!isset($options['link'][$element->{'@id'}])) {
} $options['link'][$element->{'@id'}] = [];
return $rval; }
} $options['link'][$element->{'@id'}][] = [
'expanded' => $element, 'compacted' => $rval];
// FIXME: avoid misuse of active property as an expanded property? }
$inside_reverse = ($active_property === '@reverse'); return $rval;
}
$rval = new stdClass();
// FIXME: avoid misuse of active property as an expanded property?
if($options['link'] && property_exists($element, '@id')) { $inside_reverse = ($active_property === '@reverse');
// store linked element
if(!isset($options['link'][$element->{'@id'}])) { $rval = new stdClass();
$options['link'][$element->{'@id'}] = array();
} if ($options['link'] && property_exists($element, '@id')) {
$options['link'][$element->{'@id'}][] = array( // store linked element
'expanded' => $element, 'compacted' => $rval); if (!isset($options['link'][$element->{'@id'}])) {
} $options['link'][$element->{'@id'}] = [];
}
// process element keys in order $options['link'][$element->{'@id'}][] = [
$keys = array_keys((array)$element); 'expanded' => $element, 'compacted' => $rval];
sort($keys); }
foreach($keys as $expanded_property) {
$expanded_value = $element->{$expanded_property}; // process element keys in order
$keys = array_keys((array) $element);
// compact @id and @type(s) sort($keys);
if($expanded_property === '@id' || $expanded_property === '@type') { foreach ($keys as $expanded_property) {
if(is_string($expanded_value)) { $expanded_value = $element->{$expanded_property};
// compact single @id
$compacted_value = $this->_compactIri( // compact @id and @type(s)
$active_ctx, $expanded_value, null, if ($expanded_property === '@id' || $expanded_property === '@type') {
array('vocab' => ($expanded_property === '@type'))); if (is_string($expanded_value)) {
} else { // compact single @id
// expanded value must be a @type array $compacted_value = $this->_compactIri(
$compacted_value = array(); $active_ctx, $expanded_value, null, ['vocab' => ($expanded_property === '@type')]);
foreach($expanded_value as $ev) { } else {
$compacted_value[] = $this->_compactIri( // expanded value must be a @type array
$active_ctx, $ev, null, array('vocab' => true)); $compacted_value = [];
} foreach ($expanded_value as $ev) {
} $compacted_value[] = $this->_compactIri(
$active_ctx, $ev, null, ['vocab' => true]);
// use keyword alias and add value }
$alias = $this->_compactIri($active_ctx, $expanded_property); }
$is_array = (is_array($compacted_value) &&
count($expanded_value) === 0); // use keyword alias and add value
self::addValue( $alias = $this->_compactIri($active_ctx, $expanded_property);
$rval, $alias, $compacted_value, $is_array = (is_array($compacted_value) &&
array('propertyIsArray' => $is_array)); count($expanded_value) === 0);
continue; self::addValue(
} $rval, $alias, $compacted_value, ['propertyIsArray' => $is_array]);
continue;
// handle @reverse }
if($expanded_property === '@reverse') {
// recursively compact expanded value // handle @reverse
$compacted_value = $this->_compact( if ($expanded_property === '@reverse') {
$active_ctx, '@reverse', $expanded_value, $options); // recursively compact expanded value
$compacted_value = $this->_compact(
// handle double-reversed properties $active_ctx, '@reverse', $expanded_value, $options);
foreach($compacted_value as $compacted_property => $value) {
if(property_exists($active_ctx->mappings, $compacted_property) && // handle double-reversed properties
$active_ctx->mappings->{$compacted_property} && foreach ($compacted_value as $compacted_property => $value) {
$active_ctx->mappings->{$compacted_property}->reverse) { if (property_exists($active_ctx->mappings, $compacted_property) &&
$container = self::getContextValue( $active_ctx->mappings->{$compacted_property} &&
$active_ctx, $compacted_property, '@container'); $active_ctx->mappings->{$compacted_property}->reverse) {
$use_array = ($container === '@set' || $container = self::getContextValue(
!$options['compactArrays']); $active_ctx, $compacted_property, '@container');
self::addValue( $use_array = ($container === '@set' ||
$rval, $compacted_property, $value, !$options['compactArrays']);
array('propertyIsArray' => $use_array)); self::addValue(
unset($compacted_value->{$compacted_property}); $rval, $compacted_property, $value, ['propertyIsArray' => $use_array]);
} unset($compacted_value->{$compacted_property});
} }
}
if(count(array_keys((array)$compacted_value)) > 0) {
// use keyword alias and add value if (count(array_keys((array) $compacted_value)) > 0) {
$alias = $this->_compactIri($active_ctx, $expanded_property); // use keyword alias and add value
self::addValue($rval, $alias, $compacted_value); $alias = $this->_compactIri($active_ctx, $expanded_property);
} self::addValue($rval, $alias, $compacted_value);
}
continue;
} continue;
}
// handle @index property
if($expanded_property === '@index') { // handle @index property
// drop @index if inside an @index container if ($expanded_property === '@index') {
$container = self::getContextValue( // drop @index if inside an @index container
$active_ctx, $active_property, '@container'); $container = self::getContextValue(
if($container === '@index') { $active_ctx, $active_property, '@container');
continue; if ($container === '@index') {
} continue;
}
// use keyword alias and add value
$alias = $this->_compactIri($active_ctx, $expanded_property); // use keyword alias and add value
self::addValue($rval, $alias, $expanded_value); $alias = $this->_compactIri($active_ctx, $expanded_property);
continue; self::addValue($rval, $alias, $expanded_value);
} continue;
}
// skip array processing for keywords that aren't @graph or @list
if($expanded_property !== '@graph' && $expanded_property !== '@list' && // skip array processing for keywords that aren't @graph or @list
self::_isKeyword($expanded_property)) { if ($expanded_property !== '@graph' && $expanded_property !== '@list' &&
// use keyword alias and add value as is self::_isKeyword($expanded_property)) {
$alias = $this->_compactIri($active_ctx, $expanded_property); // use keyword alias and add value as is
self::addValue($rval, $alias, $expanded_value); $alias = $this->_compactIri($active_ctx, $expanded_property);
continue; self::addValue($rval, $alias, $expanded_value);
} continue;
}
// Note: expanded value must be an array due to expansion algorithm.
// Note: expanded value must be an array due to expansion algorithm.
// preserve empty arrays // preserve empty arrays
if(count($expanded_value) === 0) { if (count($expanded_value) === 0) {
$item_active_property = $this->_compactIri( $item_active_property = $this->_compactIri(
$active_ctx, $expanded_property, $expanded_value, $active_ctx, $expanded_property, $expanded_value, ['vocab' => true], $inside_reverse);
array('vocab' => true), $inside_reverse); self::addValue(
self::addValue( $rval, $item_active_property, [], ['propertyIsArray' => true]);
$rval, $item_active_property, array(), }
array('propertyIsArray' => true));
} // recusively process array values
foreach ($expanded_value as $expanded_item) {
// recusively process array values // compact property and get container type
foreach($expanded_value as $expanded_item) { $item_active_property = $this->_compactIri(
// compact property and get container type $active_ctx, $expanded_property, $expanded_item, ['vocab' => true], $inside_reverse);
$item_active_property = $this->_compactIri( $container = self::getContextValue(
$active_ctx, $expanded_property, $expanded_item, $active_ctx, $item_active_property, '@container');
array('vocab' => true), $inside_reverse);
$container = self::getContextValue( // get @list value if appropriate
$active_ctx, $item_active_property, '@container'); $is_list = self::_isList($expanded_item);
$list = null;
// get @list value if appropriate if ($is_list) {
$is_list = self::_isList($expanded_item); $list = $expanded_item->{'@list'};
$list = null; }
if($is_list) {
$list = $expanded_item->{'@list'}; // recursively compact expanded item
} $compacted_item = $this->_compact(
$active_ctx, $item_active_property, $is_list ? $list : $expanded_item, $options);
// recursively compact expanded item
$compacted_item = $this->_compact( // handle @list
$active_ctx, $item_active_property, if ($is_list) {
$is_list ? $list : $expanded_item, $options); // ensure @list value is an array
$compacted_item = self::arrayify($compacted_item);
// handle @list
if($is_list) { if ($container !== '@list') {
// ensure @list value is an array // wrap using @list alias
$compacted_item = self::arrayify($compacted_item); $compacted_item = (object) [
$this->_compactIri($active_ctx, '@list') => $compacted_item];
if($container !== '@list') {
// wrap using @list alias // include @index from expanded @list, if any
$compacted_item = (object)array( if (property_exists($expanded_item, '@index')) {
$this->_compactIri($active_ctx, '@list') => $compacted_item); $compacted_item->{$this->_compactIri($active_ctx, '@index')} = $expanded_item->{'@index'};
}
// include @index from expanded @list, if any } else if (property_exists($rval, $item_active_property)) {
if(property_exists($expanded_item, '@index')) { // can't use @list container for more than 1 list
$compacted_item->{$this->_compactIri($active_ctx, '@index')} = throw new JsonLdException(
$expanded_item->{'@index'}; 'JSON-LD compact error; property has a "@list" @container ' .
} 'rule but there is more than a single @list that matches ' .
} else if(property_exists($rval, $item_active_property)) { 'the compacted term in the document. Compaction might mix ' .
// can't use @list container for more than 1 list 'unwanted items into the list.', 'jsonld.SyntaxError', 'compaction to list of lists');
throw new JsonLdException( }
'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 ' . // handle language and index maps
'unwanted items into the list.', 'jsonld.SyntaxError', if ($container === '@language' || $container === '@index') {
'compaction to list of lists'); // get or create the map object
} if (property_exists($rval, $item_active_property)) {
} $map_object = $rval->{$item_active_property};
} else {
// handle language and index maps $rval->{$item_active_property} = $map_object = new stdClass();
if($container === '@language' || $container === '@index') { }
// get or create the map object
if(property_exists($rval, $item_active_property)) { // if container is a language map, simplify compacted value to
$map_object = $rval->{$item_active_property}; // a simple string
} else { if ($container === '@language' && self::_isValue($compacted_item)) {
$rval->{$item_active_property} = $map_object = new stdClass(); $compacted_item = $compacted_item->{'@value'};
} }
// if container is a language map, simplify compacted value to // add compact value to map object using key from expanded value
// a simple string // based on the container type
if($container === '@language' && self::_isValue($compacted_item)) { self::addValue(
$compacted_item = $compacted_item->{'@value'}; $map_object, $expanded_item->{$container}, $compacted_item);
} } else {
// use an array if: compactArrays flag is false,
// add compact value to map object using key from expanded value // @container is @set or @list, value is an empty
// based on the container type // array, or key is @graph
self::addValue( $is_array = (!$options['compactArrays'] ||
$map_object, $expanded_item->{$container}, $compacted_item); $container === '@set' || $container === '@list' ||
} else { (is_array($compacted_item) && count($compacted_item) === 0) ||
// use an array if: compactArrays flag is false, $expanded_property === '@list' ||
// @container is @set or @list, value is an empty $expanded_property === '@graph');
// array, or key is @graph
$is_array = (!$options['compactArrays'] || // add compact value
$container === '@set' || $container === '@list' || self::addValue(
(is_array($compacted_item) && count($compacted_item) === 0) || $rval, $item_active_property, $compacted_item, ['propertyIsArray' => $is_array]);
$expanded_property === '@list' || }
$expanded_property === '@graph'); }
}
// add compact value
self::addValue( return $rval;
$rval, $item_active_property, $compacted_item, }
array('propertyIsArray' => $is_array));
} // only primitives remain which are already compact
} return $element;
} }
return $rval; /**
} * Recursively expands an element using the given context. Any context in
* the element will be removed. All context URLs must have been retrieved
// only primitives remain which are already compact * before calling this method.
return $element; *
} * @param stdClass $active_ctx the active context to use.
* @param mixed $active_property the property for the element, null for none.
/** * @param mixed $element the element to expand.
* Recursively expands an element using the given context. Any context in * @param assoc $options the expansion options.
* the element will be removed. All context URLs must have been retrieved * @param bool $inside_list true if the property is a list, false if not.
* before calling this method. *
* * @return mixed the expanded value.
* @param stdClass $active_ctx the active context to use. */
* @param mixed $active_property the property for the element, null for none. protected function _expand(
* @param mixed $element the element to expand. $active_ctx, $active_property, $element, $options, $inside_list)
* @param assoc $options the expansion options. {
* @param bool $inside_list true if the property is a list, false if not. // nothing to expand
* if ($element === null) {
* @return mixed the expanded value. return $element;
*/ }
protected function _expand(
$active_ctx, $active_property, $element, $options, $inside_list) { // recursively expand array
// nothing to expand if (is_array($element)) {
if($element === null) { $rval = [];
return $element; $container = self::getContextValue(
} $active_ctx, $active_property, '@container');
$inside_list = $inside_list || $container === '@list';
// recursively expand array foreach ($element as $e) {
if(is_array($element)) { // expand element
$rval = array(); $e = $this->_expand(
$container = self::getContextValue( $active_ctx, $active_property, $e, $options, $inside_list);
$active_ctx, $active_property, '@container'); if ($inside_list && (is_array($e) || self::_isList($e))) {
$inside_list = $inside_list || $container === '@list'; // lists of lists are illegal
foreach($element as $e) { throw new JsonLdException(
// expand element 'Invalid JSON-LD syntax; lists of lists are not permitted.', 'jsonld.SyntaxError', 'list of lists');
$e = $this->_expand( }
$active_ctx, $active_property, $e, $options, $inside_list); // drop null values
if($inside_list && (is_array($e) || self::_isList($e))) { if ($e !== null) {
// lists of lists are illegal if (is_array($e)) {
throw new JsonLdException( $rval = array_merge($rval, $e);
'Invalid JSON-LD syntax; lists of lists are not permitted.', } else {
'jsonld.SyntaxError', 'list of lists'); $rval[] = $e;
} }
// drop null values }
if($e !== null) { }
if(is_array($e)) { return $rval;
$rval = array_merge($rval, $e); }
} else {
$rval[] = $e; if (!is_object($element)) {
} // drop free-floating scalars that are not in lists
} if (!$inside_list &&
} ($active_property === null ||
return $rval; $this->_expandIri($active_ctx, $active_property, ['vocab' => true]) === '@graph')) {
} return null;
}
if(!is_object($element)) {
// drop free-floating scalars that are not in lists // expand element according to value expansion rules
if(!$inside_list && return $this->_expandValue($active_ctx, $active_property, $element);
($active_property === null || }
$this->_expandIri($active_ctx, $active_property,
array('vocab' => true)) === '@graph')) { // recursively expand object:
return null; // if element has a context, process it
} if (property_exists($element, '@context')) {
$active_ctx = $this->_processContext(
// expand element according to value expansion rules $active_ctx, $element->{'@context'}, $options);
return $this->_expandValue($active_ctx, $active_property, $element); }
}
// expand the active property
// recursively expand object: $expanded_active_property = $this->_expandIri(
$active_ctx, $active_property, ['vocab' => true]);
// if element has a context, process it
if(property_exists($element, '@context')) { $rval = new stdClass();
$active_ctx = $this->_processContext( $keys = array_keys((array) $element);
$active_ctx, $element->{'@context'}, $options); sort($keys);
} foreach ($keys as $key) {
$value = $element->{$key};
// expand the active property
$expanded_active_property = $this->_expandIri( if ($key === '@context') {
$active_ctx, $active_property, array('vocab' => true)); continue;
}
$rval = new stdClass();
$keys = array_keys((array)$element); // expand key to IRI
sort($keys); $expanded_property = $this->_expandIri(
foreach($keys as $key) { $active_ctx, $key, ['vocab' => true]);
$value = $element->{$key};
// drop non-absolute IRI keys that aren't keywords
if($key === '@context') { if ($expanded_property === null ||
continue; !(self::_isAbsoluteIri($expanded_property) ||
} self::_isKeyword($expanded_property))) {
continue;
// expand key to IRI }
$expanded_property = $this->_expandIri(
$active_ctx, $key, array('vocab' => true)); if (self::_isKeyword($expanded_property)) {
if ($expanded_active_property === '@reverse') {
// drop non-absolute IRI keys that aren't keywords throw new JsonLdException(
if($expanded_property === null || 'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' .
!(self::_isAbsoluteIri($expanded_property) || 'property.', 'jsonld.SyntaxError', 'invalid reverse property map', ['value' => $value]);
self::_isKeyword($expanded_property))) { }
continue; if (property_exists($rval, $expanded_property)) {
} throw new JsonLdException(
'Invalid JSON-LD syntax; colliding keywords detected.', 'jsonld.SyntaxError', 'colliding keywords', ['keyword' => $expanded_property]);
if(self::_isKeyword($expanded_property)) { }
if($expanded_active_property === '@reverse') { }
throw new JsonLdException(
'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' . // syntax error if @id is not a string
'property.', 'jsonld.SyntaxError', 'invalid reverse property map', if ($expanded_property === '@id' && !is_string($value)) {
array('value' => $value)); if (!isset($options['isFrame']) || !$options['isFrame']) {
} throw new JsonLdException(
if(property_exists($rval, $expanded_property)) { 'Invalid JSON-LD syntax; "@id" value must a string.', 'jsonld.SyntaxError', 'invalid @id value', ['value' => $value]);
throw new JsonLdException( }
'Invalid JSON-LD syntax; colliding keywords detected.', if (!is_object($value)) {
'jsonld.SyntaxError', 'colliding keywords', throw new JsonLdException(
array('keyword' => $expanded_property)); 'Invalid JSON-LD syntax; "@id" value must a string or an object.', 'jsonld.SyntaxError', 'invalid @id value', ['value' => $value]);
} }
} }
// syntax error if @id is not a string // validate @type value
if($expanded_property === '@id' && !is_string($value)) { if ($expanded_property === '@type') {
if(!isset($options['isFrame']) || !$options['isFrame']) { $this->_validateTypeValue($value);
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@id" value must a string.',
'jsonld.SyntaxError', 'invalid @id value', // @graph must be an array or an object
array('value' => $value)); if ($expanded_property === '@graph' &&
} !(is_object($value) || is_array($value))) {
if(!is_object($value)) { throw new JsonLdException(
throw new JsonLdException( 'Invalid JSON-LD syntax; "@graph" value must not be an ' .
'Invalid JSON-LD syntax; "@id" value must a string or an object.', 'object or an array.', 'jsonld.SyntaxError', 'invalid @graph value', ['value' => $value]);
'jsonld.SyntaxError', 'invalid @id value', }
array('value' => $value));
} // @value must not be an object or an array
} if ($expanded_property === '@value' &&
(is_object($value) || is_array($value))) {
// validate @type value throw new JsonLdException(
if($expanded_property === '@type') { 'Invalid JSON-LD syntax; "@value" value must not be an ' .
$this->_validateTypeValue($value); 'object or an array.', 'jsonld.SyntaxError', 'invalid value object value', ['value' => $value]);
} }
// @graph must be an array or an object // @language must be a string
if($expanded_property === '@graph' && if ($expanded_property === '@language') {
!(is_object($value) || is_array($value))) { if ($value === null) {
throw new JsonLdException( // drop null @language values, they expand as if they didn't exist
'Invalid JSON-LD syntax; "@graph" value must not be an ' . continue;
'object or an array.', 'jsonld.SyntaxError', }
'invalid @graph value', array('value' => $value)); if (!is_string($value)) {
} throw new JsonLdException(
'Invalid JSON-LD syntax; "@language" value must not be a string.', 'jsonld.SyntaxError', 'invalid language-tagged string', ['value' => $value]);
// @value must not be an object or an array }
if($expanded_property === '@value' && // ensure language value is lowercase
(is_object($value) || is_array($value))) { $value = strtolower($value);
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@value" value must not be an ' .
'object or an array.', 'jsonld.SyntaxError', // @index must be a string
'invalid value object value', array('value' => $value)); if ($expanded_property === '@index') {
} if (!is_string($value)) {
throw new JsonLdException(
// @language must be a string 'Invalid JSON-LD syntax; "@index" value must be a string.', 'jsonld.SyntaxError', 'invalid @index value', ['value' => $value]);
if($expanded_property === '@language') { }
if($value === null) { }
// drop null @language values, they expand as if they didn't exist
continue; // @reverse must be an object
} if ($expanded_property === '@reverse') {
if(!is_string($value)) { if (!is_object($value)) {
throw new JsonLdException( throw new JsonLdException(
'Invalid JSON-LD syntax; "@language" value must not be a string.', 'Invalid JSON-LD syntax; "@reverse" value must be an object.', 'jsonld.SyntaxError', 'invalid @reverse value', ['value' => $value]);
'jsonld.SyntaxError', 'invalid language-tagged string', }
array('value' => $value));
} $expanded_value = $this->_expand(
// ensure language value is lowercase $active_ctx, '@reverse', $value, $options, $inside_list);
$value = strtolower($value);
} // properties double-reversed
if (property_exists($expanded_value, '@reverse')) {
// @index must be a string foreach ($expanded_value->{'@reverse'} as $rproperty => $rvalue) {
if($expanded_property === '@index') { self::addValue(
if(!is_string($value)) { $rval, $rproperty, $rvalue, ['propertyIsArray' => true]);
throw new JsonLdException( }
'Invalid JSON-LD syntax; "@index" value must be a string.', }
'jsonld.SyntaxError', 'invalid @index value',
array('value' => $value)); // FIXME: can this be merged with code below to simplify?
} // merge in all reversed properties
} if (property_exists($rval, '@reverse')) {
$reverse_map = $rval->{'@reverse'};
// @reverse must be an object } else {
if($expanded_property === '@reverse') { $reverse_map = null;
if(!is_object($value)) { }
throw new JsonLdException( foreach ($expanded_value as $property => $items) {
'Invalid JSON-LD syntax; "@reverse" value must be an object.', if ($property === '@reverse') {
'jsonld.SyntaxError', 'invalid @reverse value', continue;
array('value' => $value)); }
} if ($reverse_map === null) {
$reverse_map = $rval->{'@reverse'} = new stdClass();
$expanded_value = $this->_expand( }
$active_ctx, '@reverse', $value, $options, $inside_list); self::addValue(
$reverse_map, $property, [], ['propertyIsArray' => true]);
// properties double-reversed foreach ($items as $item) {
if(property_exists($expanded_value, '@reverse')) { if (self::_isValue($item) || self::_isList($item)) {
foreach($expanded_value->{'@reverse'} as $rproperty => $rvalue) { throw new JsonLdException(
self::addValue( 'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
$rval, $rproperty, $rvalue, array('propertyIsArray' => true)); '@value or an @list.', 'jsonld.SyntaxError', 'invalid reverse property value', ['value' => $expanded_value]);
} }
} self::addValue(
$reverse_map, $property, $item, ['propertyIsArray' => true]);
// FIXME: can this be merged with code below to simplify? }
// merge in all reversed properties }
if(property_exists($rval, '@reverse')) {
$reverse_map = $rval->{'@reverse'}; continue;
} else { }
$reverse_map = null;
} $container = self::getContextValue($active_ctx, $key, '@container');
foreach($expanded_value as $property => $items) {
if($property === '@reverse') { if ($container === '@language' && is_object($value)) {
continue; // handle language map container (skip if value is not an object)
} $expanded_value = $this->_expandLanguageMap($value);
if($reverse_map === null) { } else if ($container === '@index' && is_object($value)) {
$reverse_map = $rval->{'@reverse'} = new stdClass(); // handle index container (skip if value is not an object)
} $expanded_value = [];
self::addValue( $value_keys = array_keys((array) $value);
$reverse_map, $property, array(), sort($value_keys);
array('propertyIsArray' => true)); foreach ($value_keys as $value_key) {
foreach($items as $item) { $val = $value->{$value_key};
if(self::_isValue($item) || self::_isList($item)) { $val = self::arrayify($val);
throw new JsonLdException( $val = $this->_expand($active_ctx, $key, $val, $options, false);
'Invalid JSON-LD syntax; "@reverse" value must not be a ' + foreach ($val as $item) {
'@value or an @list.', 'jsonld.SyntaxError', if (!property_exists($item, '@index')) {
'invalid reverse property value', $item->{'@index'} = $value_key;
array('value' => $expanded_value)); }
} $expanded_value[] = $item;
self::addValue( }
$reverse_map, $property, $item, }
array('propertyIsArray' => true)); } else {
} // recurse into @list or @set
} $is_list = ($expanded_property === '@list');
if ($is_list || $expanded_property === '@set') {
continue; $next_active_property = $active_property;
} if ($is_list && $expanded_active_property === '@graph') {
$next_active_property = null;
$container = self::getContextValue($active_ctx, $key, '@container'); }
$expanded_value = $this->_expand(
if($container === '@language' && is_object($value)) { $active_ctx, $next_active_property, $value, $options, $is_list);
// handle language map container (skip if value is not an object) if ($is_list && self::_isList($expanded_value)) {
$expanded_value = $this->_expandLanguageMap($value); throw new JsonLdException(
} else if($container === '@index' && is_object($value)) { 'Invalid JSON-LD syntax; lists of lists are not permitted.', 'jsonld.SyntaxError', 'list of lists');
// handle index container (skip if value is not an object) }
$expanded_value = array(); } else {
$value_keys = array_keys((array)$value); // recursively expand value with key as new active property
sort($value_keys); $expanded_value = $this->_expand(
foreach($value_keys as $value_key) { $active_ctx, $key, $value, $options, false);
$val = $value->{$value_key}; }
$val = self::arrayify($val); }
$val = $this->_expand($active_ctx, $key, $val, $options, false);
foreach($val as $item) { // drop null values if property is not @value
if(!property_exists($item, '@index')) { if ($expanded_value === null && $expanded_property !== '@value') {
$item->{'@index'} = $value_key; continue;
} }
$expanded_value[] = $item;
} // convert expanded value to @list if container specifies it
} if ($expanded_property !== '@list' && !self::_isList($expanded_value) &&
} else { $container === '@list') {
// recurse into @list or @set // ensure expanded value is an array
$is_list = ($expanded_property === '@list'); $expanded_value = (object) [
if($is_list || $expanded_property === '@set') { '@list' => self::arrayify($expanded_value)];
$next_active_property = $active_property; }
if($is_list && $expanded_active_property === '@graph') {
$next_active_property = null; // FIXME: can this be merged with code above to simplify?
} // merge in reverse properties
$expanded_value = $this->_expand( if (property_exists($active_ctx->mappings, $key) &&
$active_ctx, $next_active_property, $value, $options, $is_list); $active_ctx->mappings->{$key} &&
if($is_list && self::_isList($expanded_value)) { $active_ctx->mappings->{$key}->reverse) {
throw new JsonLdException( if (property_exists($rval, '@reverse')) {
'Invalid JSON-LD syntax; lists of lists are not permitted.', $reverse_map = $rval->{'@reverse'};
'jsonld.SyntaxError', 'list of lists'); } else {
} $reverse_map = $rval->{'@reverse'} = new stdClass();
} else { }
// recursively expand value with key as new active property $expanded_value = self::arrayify($expanded_value);
$expanded_value = $this->_expand( foreach ($expanded_value as $item) {
$active_ctx, $key, $value, $options, false); if (self::_isValue($item) || self::_isList($item)) {
} throw new JsonLdException(
} 'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
'@value or an @list.', 'jsonld.SyntaxError', 'invalid reverse property value', ['value' => $expanded_value]);
// drop null values if property is not @value }
if($expanded_value === null && $expanded_property !== '@value') { self::addValue(
continue; $reverse_map, $expanded_property, $item, ['propertyIsArray' => true]);
} }
continue;
// convert expanded value to @list if container specifies it }
if($expanded_property !== '@list' && !self::_isList($expanded_value) &&
$container === '@list') { // add value for property
// ensure expanded value is an array // use an array except for certain keywords
$expanded_value = (object)array( $use_array = (!in_array(
'@list' => self::arrayify($expanded_value)); $expanded_property, [
} '@index', '@id', '@type', '@value', '@language']));
self::addValue(
// FIXME: can this be merged with code above to simplify? $rval, $expanded_property, $expanded_value, ['propertyIsArray' => $use_array]);
// merge in reverse properties }
if(property_exists($active_ctx->mappings, $key) &&
$active_ctx->mappings->{$key} && // get property count on expanded output
$active_ctx->mappings->{$key}->reverse) { $keys = array_keys((array) $rval);
if(property_exists($rval, '@reverse')) { $count = count($keys);
$reverse_map = $rval->{'@reverse'};
} else { // @value must only have @language or @type
$reverse_map = $rval->{'@reverse'} = new stdClass(); if (property_exists($rval, '@value')) {
} // @value must only have @language or @type
$expanded_value = self::arrayify($expanded_value); if (property_exists($rval, '@type') &&
foreach($expanded_value as $item) { property_exists($rval, '@language')) {
if(self::_isValue($item) || self::_isList($item)) { throw new JsonLdException(
throw new JsonLdException( 'Invalid JSON-LD syntax; an element containing "@value" may not ' .
'Invalid JSON-LD syntax; "@reverse" value must not be a ' + 'contain both "@type" and "@language".', 'jsonld.SyntaxError', 'invalid value object', ['element' => $rval]);
'@value or an @list.', 'jsonld.SyntaxError', }
'invalid reverse property value', $valid_count = $count - 1;
array('value' => $expanded_value)); if (property_exists($rval, '@type')) {
} $valid_count -= 1;
self::addValue( }
$reverse_map, $expanded_property, $item, if (property_exists($rval, '@index')) {
array('propertyIsArray' => true)); $valid_count -= 1;
} }
continue; if (property_exists($rval, '@language')) {
} $valid_count -= 1;
}
// add value for property if ($valid_count !== 0) {
// use an array except for certain keywords throw new JsonLdException(
$use_array = (!in_array( 'Invalid JSON-LD syntax; an element containing "@value" may only ' .
$expanded_property, array( 'have an "@index" property and at most one other property ' .
'@index', '@id', '@type', '@value', '@language'))); 'which can be "@type" or "@language".', 'jsonld.SyntaxError', 'invalid value object', ['element' => $rval]);
self::addValue( }
$rval, $expanded_property, $expanded_value, // drop null @values
array('propertyIsArray' => $use_array)); if ($rval->{'@value'} === null) {
} $rval = null;
} else if (property_exists($rval, '@language') &&
// get property count on expanded output !is_string($rval->{'@value'})) {
$keys = array_keys((array)$rval); // if @language is present, @value must be a string
$count = count($keys); throw new JsonLdException(
'Invalid JSON-LD syntax; only strings may be language-tagged.', 'jsonld.SyntaxError', 'invalid language-tagged value', ['element' => $rval]);
// @value must only have @language or @type } else if (property_exists($rval, '@type') &&
if(property_exists($rval, '@value')) { (!self::_isAbsoluteIri($rval->{'@type'}) ||
// @value must only have @language or @type strpos($rval->{'@type'}, '_:') === 0)) {
if(property_exists($rval, '@type') && throw new JsonLdException(
property_exists($rval, '@language')) { 'Invalid JSON-LD syntax; an element containing "@value" ' .
throw new JsonLdException( 'and "@type" must have an absolute IRI for the value ' .
'Invalid JSON-LD syntax; an element containing "@value" may not ' . 'of "@type".', 'jsonld.SyntaxError', 'invalid typed value', ['element' => $rval]);
'contain both "@type" and "@language".', }
'jsonld.SyntaxError', 'invalid value object', } else if (property_exists($rval, '@type') && !is_array($rval->{'@type'})) {
array('element' => $rval)); // convert @type to an array
} $rval->{'@type'} = [$rval->{'@type'}];
$valid_count = $count - 1; } else if (property_exists($rval, '@set') ||
if(property_exists($rval, '@type')) { property_exists($rval, '@list')) {
$valid_count -= 1; // handle @set and @list
} if ($count > 1 && !($count === 2 && property_exists($rval, '@index'))) {
if(property_exists($rval, '@index')) { throw new JsonLdException(
$valid_count -= 1; 'Invalid JSON-LD syntax; if an element has the property "@set" ' .
} 'or "@list", then it can have at most one other property that is ' .
if(property_exists($rval, '@language')) { '"@index".', 'jsonld.SyntaxError', 'invalid set or list object', ['element' => $rval]);
$valid_count -= 1; }
} // optimize away @set
if($valid_count !== 0) { if (property_exists($rval, '@set')) {
throw new JsonLdException( $rval = $rval->{'@set'};
'Invalid JSON-LD syntax; an element containing "@value" may only ' . $keys = array_keys((array) $rval);
'have an "@index" property and at most one other property ' . $count = count($keys);
'which can be "@type" or "@language".', }
'jsonld.SyntaxError', 'invalid value object', } else if ($count === 1 && property_exists($rval, '@language')) {
array('element' => $rval)); // drop objects with only @language
} $rval = null;
// drop null @values }
if($rval->{'@value'} === null) {
$rval = null; // drop certain top-level objects that do not occur in lists
} else if(property_exists($rval, '@language') && if (is_object($rval) &&
!is_string($rval->{'@value'})) { !$options['keepFreeFloatingNodes'] && !$inside_list &&
// if @language is present, @value must be a string ($active_property === null || $expanded_active_property === '@graph')) {
throw new JsonLdException( // drop empty object or top-level @value/@list, or object with only @id
'Invalid JSON-LD syntax; only strings may be language-tagged.', if ($count === 0 || property_exists($rval, '@value') ||
'jsonld.SyntaxError', 'invalid language-tagged value', property_exists($rval, '@list') ||
array('element' => $rval)); ($count === 1 && property_exists($rval, '@id'))) {
} else if(property_exists($rval, '@type') && $rval = null;
(!self::_isAbsoluteIri($rval->{'@type'}) || }
strpos($rval->{'@type'}, '_:') === 0)) { }
throw new JsonLdException(
'Invalid JSON-LD syntax; an element containing "@value" ' . return $rval;
'and "@type" must have an absolute IRI for the value ' . }
'of "@type".', 'jsonld.SyntaxError', 'invalid typed value',
array('element' => $rval)); /**
} * Performs JSON-LD flattening.
} else if(property_exists($rval, '@type') && !is_array($rval->{'@type'})) { *
// convert @type to an array * @param array $input the expanded JSON-LD to flatten.
$rval->{'@type'} = array($rval->{'@type'}); *
} else if(property_exists($rval, '@set') || * @return array the flattened output.
property_exists($rval, '@list')) { */
// handle @set and @list protected function _flatten($input)
if($count > 1 && !($count === 2 && property_exists($rval, '@index'))) { {
throw new JsonLdException( // produce a map of all subjects and name each bnode
'Invalid JSON-LD syntax; if an element has the property "@set" ' . $namer = new UniqueNamer('_:b');
'or "@list", then it can have at most one other property that is ' . $graphs = (object) ['@default' => new stdClass()];
'"@index".', 'jsonld.SyntaxError', 'invalid set or list object', $this->_createNodeMap($input, $graphs, '@default', $namer);
array('element' => $rval));
} // add all non-default graphs to default graph
// optimize away @set $default_graph = $graphs->{'@default'};
if(property_exists($rval, '@set')) { $graph_names = array_keys((array) $graphs);
$rval = $rval->{'@set'}; foreach ($graph_names as $graph_name) {
$keys = array_keys((array)$rval); if ($graph_name === '@default') {
$count = count($keys); continue;
} }
} else if($count === 1 && property_exists($rval, '@language')) { $node_map = $graphs->{$graph_name};
// drop objects with only @language if (!property_exists($default_graph, $graph_name)) {
$rval = null; $default_graph->{$graph_name} = (object) [
} '@id' => $graph_name, '@graph' => []];
}
// drop certain top-level objects that do not occur in lists $subject = $default_graph->{$graph_name};
if(is_object($rval) && if (!property_exists($subject, '@graph')) {
!$options['keepFreeFloatingNodes'] && !$inside_list && $subject->{'@graph'} = [];
($active_property === null || $expanded_active_property === '@graph')) { }
// drop empty object or top-level @value/@list, or object with only @id $ids = array_keys((array) $node_map);
if($count === 0 || property_exists($rval, '@value') || sort($ids);
property_exists($rval, '@list') || foreach ($ids as $id) {
($count === 1 && property_exists($rval, '@id'))) { $node = $node_map->{$id};
$rval = null; // only add full subjects
} if (!self::_isSubjectReference($node)) {
} $subject->{'@graph'}[] = $node;
}
return $rval; }
} }
/** // produce flattened output
* Performs JSON-LD flattening. $flattened = [];
* $keys = array_keys((array) $default_graph);
* @param array $input the expanded JSON-LD to flatten. sort($keys);
* foreach ($keys as $key) {
* @return array the flattened output. $node = $default_graph->{$key};
*/ // only add full subjects to top-level
protected function _flatten($input) { if (!self::_isSubjectReference($node)) {
// produce a map of all subjects and name each bnode $flattened[] = $node;
$namer = new UniqueNamer('_:b'); }
$graphs = (object)array('@default' => new stdClass()); }
$this->_createNodeMap($input, $graphs, '@default', $namer); return $flattened;
}
// add all non-default graphs to default graph
$default_graph = $graphs->{'@default'}; /**
$graph_names = array_keys((array)$graphs); * Performs JSON-LD framing.
foreach($graph_names as $graph_name) { *
if($graph_name === '@default') { * @param array $input the expanded JSON-LD to frame.
continue; * @param array $frame the expanded JSON-LD frame to use.
} * @param assoc $options the framing options.
$node_map = $graphs->{$graph_name}; *
if(!property_exists($default_graph, $graph_name)) { * @return array the framed output.
$default_graph->{$graph_name} = (object)array( */
'@id' => $graph_name, '@graph' => array()); protected function _frame($input, $frame, $options)
} {
$subject = $default_graph->{$graph_name}; // create framing state
if(!property_exists($subject, '@graph')) { $state = (object) [
$subject->{'@graph'} = array(); 'options' => $options,
} 'graphs' => (object) [
$ids = array_keys((array)$node_map); '@default' => new stdClass(),
sort($ids); '@merged' => new stdClass()],
foreach($ids as $id) { 'subjectStack' => [],
$node = $node_map->{$id}; 'link' => new stdClass()];
// only add full subjects
if(!self::_isSubjectReference($node)) { // produce a map of all graphs and name each bnode
$subject->{'@graph'}[] = $node; // FIXME: currently uses subjects from @merged graph only
} $namer = new UniqueNamer('_:b');
} $this->_createNodeMap($input, $state->graphs, '@merged', $namer);
} $state->subjects = $state->graphs->{'@merged'};
// produce flattened output // frame the subjects
$flattened = array(); $framed = new ArrayObject();
$keys = array_keys((array)$default_graph); $keys = array_keys((array) $state->subjects);
sort($keys); sort($keys);
foreach($keys as $key) { $this->_matchFrame($state, $keys, $frame, $framed, null);
$node = $default_graph->{$key}; return (array) $framed;
// only add full subjects to top-level }
if(!self::_isSubjectReference($node)) {
$flattened[] = $node; /**
} * Performs normalization on the given RDF dataset.
} *
return $flattened; * @param stdClass $dataset the RDF dataset to normalize.
} * @param assoc $options the normalization options.
*
/** * @return mixed the normalized output.
* Performs JSON-LD framing. */
* protected function _normalize($dataset, $options)
* @param array $input the expanded JSON-LD to frame. {
* @param array $frame the expanded JSON-LD frame to use. // create quads and map bnodes to their associated quads
* @param assoc $options the framing options. $quads = [];
* $bnodes = new stdClass();
* @return array the framed output. foreach ($dataset as $graph_name => $triples) {
*/ if ($graph_name === '@default') {
protected function _frame($input, $frame, $options) { $graph_name = null;
// create framing state }
$state = (object)array( foreach ($triples as $triple) {
'options' => $options, $quad = $triple;
'graphs' => (object)array( if ($graph_name !== null) {
'@default' => new stdClass(), if (strpos($graph_name, '_:') === 0) {
'@merged' => new stdClass()), $quad->name = (object) [
'subjectStack' => array(), 'type' => 'blank node', 'value' => $graph_name];
'link' => new stdClass()); } else {
$quad->name = (object) [
// produce a map of all graphs and name each bnode 'type' => 'IRI', 'value' => $graph_name];
// FIXME: currently uses subjects from @merged graph only }
$namer = new UniqueNamer('_:b'); }
$this->_createNodeMap($input, $state->graphs, '@merged', $namer); $quads[] = $quad;
$state->subjects = $state->graphs->{'@merged'};
foreach (['subject', 'object', 'name'] as $attr) {
// frame the subjects if (property_exists($quad, $attr) &&
$framed = new ArrayObject(); $quad->{$attr}->type === 'blank node') {
$keys = array_keys((array)$state->subjects); $id = $quad->{$attr}->value;
sort($keys); if (property_exists($bnodes, $id)) {
$this->_matchFrame($state, $keys, $frame, $framed, null); $bnodes->{$id}->quads[] = $quad;
return (array)$framed; } else {
} $bnodes->{$id} = (object) ['quads' => [$quad]];
}
/** }
* Performs normalization on the given RDF dataset. }
* }
* @param stdClass $dataset the RDF dataset to normalize. }
* @param assoc $options the normalization options.
* // mapping complete, start canonical naming
* @return mixed the normalized output. $namer = new UniqueNamer('_:c14n');
*/
protected function _normalize($dataset, $options) { // continue to hash bnode quads while bnodes are assigned names
// create quads and map bnodes to their associated quads $unnamed = null;
$quads = array(); $nextUnnamed = array_keys((array) $bnodes);
$bnodes = new stdClass(); $duplicates = null;
foreach($dataset as $graph_name => $triples) { do {
if($graph_name === '@default') { $unnamed = $nextUnnamed;
$graph_name = null; $nextUnnamed = [];
} $duplicates = new stdClass();
foreach($triples as $triple) { $unique = new stdClass();
$quad = $triple; foreach ($unnamed as $bnode) {
if($graph_name !== null) { // hash quads for each unnamed bnode
if(strpos($graph_name, '_:') === 0) { $hash = $this->_hashQuads($bnode, $bnodes, $namer);
$quad->name = (object)array(
'type' => 'blank node', 'value' => $graph_name); // store hash as unique or a duplicate
} else { if (property_exists($duplicates, $hash)) {
$quad->name = (object)array( $duplicates->{$hash}[] = $bnode;
'type' => 'IRI', 'value' => $graph_name); $nextUnnamed[] = $bnode;
} } else if (property_exists($unique, $hash)) {
} $duplicates->{$hash} = [$unique->{$hash}, $bnode];
$quads[] = $quad; $nextUnnamed[] = $unique->{$hash};
$nextUnnamed[] = $bnode;
foreach(array('subject', 'object', 'name') as $attr) { unset($unique->{$hash});
if(property_exists($quad, $attr) && } else {
$quad->{$attr}->type === 'blank node') { $unique->{$hash} = $bnode;
$id = $quad->{$attr}->value; }
if(property_exists($bnodes, $id)) { }
$bnodes->{$id}->quads[] = $quad;
} else { // name unique bnodes in sorted hash order
$bnodes->{$id} = (object)array('quads' => array($quad)); $hashes = array_keys((array) $unique);
} sort($hashes);
} foreach ($hashes as $hash) {
} $namer->getName($unique->{$hash});
} }
} } while (count($unnamed) > count($nextUnnamed));
// mapping complete, start canonical naming // enumerate duplicate hash groups in sorted order
$namer = new UniqueNamer('_:c14n'); $hashes = array_keys((array) $duplicates);
sort($hashes);
// continue to hash bnode quads while bnodes are assigned names foreach ($hashes as $hash) {
$unnamed = null; // process group
$nextUnnamed = array_keys((array)$bnodes); $group = $duplicates->{$hash};
$duplicates = null; $results = [];
do { foreach ($group as $bnode) {
$unnamed = $nextUnnamed; // skip already-named bnodes
$nextUnnamed = array(); if ($namer->isNamed($bnode)) {
$duplicates = new stdClass(); continue;
$unique = new stdClass(); }
foreach($unnamed as $bnode) {
// hash quads for each unnamed bnode // hash bnode paths
$hash = $this->_hashQuads($bnode, $bnodes, $namer); $path_namer = new UniqueNamer('_:b');
$path_namer->getName($bnode);
// store hash as unique or a duplicate $results[] = $this->_hashPaths($bnode, $bnodes, $namer, $path_namer);
if(property_exists($duplicates, $hash)) { }
$duplicates->{$hash}[] = $bnode;
$nextUnnamed[] = $bnode; // name bnodes in hash order
} else if(property_exists($unique, $hash)) { usort($results, function($a, $b) {
$duplicates->{$hash} = array($unique->{$hash}, $bnode); $a = $a->hash;
$nextUnnamed[] = $unique->{$hash}; $b = $b->hash;
$nextUnnamed[] = $bnode; return ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
unset($unique->{$hash}); });
} else { foreach ($results as $result) {
$unique->{$hash} = $bnode; // name all bnodes in path namer in key-entry order
} foreach ($result->pathNamer->order as $bnode) {
} $namer->getName($bnode);
}
// name unique bnodes in sorted hash order }
$hashes = array_keys((array)$unique); }
sort($hashes);
foreach($hashes as $hash) { // create normalized array
$namer->getName($unique->{$hash}); $normalized = [];
}
} /* Note: At this point all bnodes in the set of RDF quads have been
while(count($unnamed) > count($nextUnnamed)); 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
// enumerate duplicate hash groups in sorted order via the 'namer' object. */
$hashes = array_keys((array)$duplicates);
sort($hashes); // update bnode names in each quad and serialize
foreach($hashes as $hash) { foreach ($quads as $quad) {
// process group foreach (['subject', 'object', 'name'] as $attr) {
$group = $duplicates->{$hash}; if (property_exists($quad, $attr) &&
$results = array(); $quad->{$attr}->type === 'blank node' &&
foreach($group as $bnode) { strpos($quad->{$attr}->value, '_:c14n') !== 0) {
// skip already-named bnodes $quad->{$attr}->value = $namer->getName($quad->{$attr}->value);
if($namer->isNamed($bnode)) { }
continue; }
} $normalized[] = $this->toNQuad($quad, property_exists($quad, 'name') ?
$quad->name->value : null);
// hash bnode paths }
$path_namer = new UniqueNamer('_:b');
$path_namer->getName($bnode); // sort normalized output
$results[] = $this->_hashPaths($bnode, $bnodes, $namer, $path_namer); sort($normalized);
}
// handle output format
// name bnodes in hash order if (isset($options['format']) && $options['format']) {
usort($results, function($a, $b) { if ($options['format'] === 'application/nquads') {
$a = $a->hash; return implode($normalized);
$b = $b->hash; }
return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); throw new JsonLdException(
}); 'Unknown output format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]);
foreach($results as $result) { }
// name all bnodes in path namer in key-entry order
foreach($result->pathNamer->order as $bnode) { // return RDF dataset
$namer->getName($bnode); return $this->parseNQuads(implode($normalized));
} }
}
} /**
* Converts an RDF dataset to JSON-LD.
// create normalized array *
$normalized = array(); * @param stdClass $dataset the RDF dataset.
* @param assoc $options the RDF serialization options.
/* 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. * @return array the JSON-LD output.
Here each quad is updated by assigning each of its bnodes its new name */
via the 'namer' object. */ protected function _fromRDF($dataset, $options)
{
// update bnode names in each quad and serialize $default_graph = new stdClass();
foreach($quads as $quad) { $graph_map = (object) ['@default' => $default_graph];
foreach(array('subject', 'object', 'name') as $attr) { $referenced_once = (object) [];
if(property_exists($quad, $attr) &&
$quad->{$attr}->type === 'blank node' && foreach ($dataset as $name => $graph) {
strpos($quad->{$attr}->value, '_:c14n') !== 0) { if (!property_exists($graph_map, $name)) {
$quad->{$attr}->value = $namer->getName($quad->{$attr}->value); $graph_map->{$name} = new stdClass();
} }
} if ($name !== '@default' && !property_exists($default_graph, $name)) {
$normalized[] = $this->toNQuad($quad, property_exists($quad, 'name') ? $default_graph->{$name} = (object) ['@id' => $name];
$quad->name->value : null); }
} $node_map = $graph_map->{$name};
foreach ($graph as $triple) {
// sort normalized output // get subject, predicate, object
sort($normalized); $s = $triple->subject->value;
$p = $triple->predicate->value;
// handle output format $o = $triple->object;
if(isset($options['format']) && $options['format']) {
if($options['format'] === 'application/nquads') { if (!property_exists($node_map, $s)) {
return implode($normalized); $node_map->{$s} = (object) ['@id' => $s];
} }
throw new JsonLdException( $node = $node_map->{$s};
'Unknown output format.',
'jsonld.UnknownFormat', null, array('format' => $options['format'])); $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];
// return RDF dataset }
return $this->parseNQuads(implode($normalized));
} if ($p === self::RDF_TYPE && !$options['useRdfType'] && $object_is_id) {
self::addValue(
/** $node, '@type', $o->value, ['propertyIsArray' => true]);
* Converts an RDF dataset to JSON-LD. continue;
* }
* @param stdClass $dataset the RDF dataset.
* @param assoc $options the RDF serialization options. $value = self::_RDFToObject($o, $options['useNativeTypes']);
* self::addValue($node, $p, $value, ['propertyIsArray' => true]);
* @return array the JSON-LD output.
*/ // object may be an RDF list/partial list node but we can't know
protected function _fromRDF($dataset, $options) { // easily until all triples are read
$default_graph = new stdClass(); if ($object_is_id) {
$graph_map = (object)array('@default' => $default_graph); if ($o->value === self::RDF_NIL) {
$referenced_once = (object)array(); $object = $node_map->{$o->value};
if (!property_exists($object, 'usages')) {
foreach($dataset as $name => $graph) { $object->usages = [];
if(!property_exists($graph_map, $name)) { }
$graph_map->{$name} = new stdClass(); $object->usages[] = (object) [
} 'node' => $node,
if($name !== '@default' && !property_exists($default_graph, $name)) { 'property' => $p,
$default_graph->{$name} = (object)array('@id' => $name); 'value' => $value];
} } else if (property_exists($referenced_once, $o->value)) {
$node_map = $graph_map->{$name}; // object referenced more than once
foreach($graph as $triple) { $referenced_once->{$o->value} = false;
// get subject, predicate, object } else {
$s = $triple->subject->value; // track single reference
$p = $triple->predicate->value; $referenced_once->{$o->value} = (object) [
$o = $triple->object; 'node' => $node,
'property' => $p,
if(!property_exists($node_map, $s)) { 'value' => $value];
$node_map->{$s} = (object)array('@id' => $s); }
} }
$node = $node_map->{$s}; }
}
$object_is_id = ($o->type === 'IRI' || $o->type === 'blank node');
if($object_is_id && !property_exists($node_map, $o->value)) { // convert linked lists to @list arrays
$node_map->{$o->value} = (object)array('@id' => $o->value); foreach ($graph_map as $name => $graph_object) {
} // no @lists to be converted, continue
if (!property_exists($graph_object, self::RDF_NIL)) {
if($p === self::RDF_TYPE && !$options['useRdfType'] && $object_is_id) { continue;
self::addValue( }
$node, '@type', $o->value, array('propertyIsArray' => true));
continue; // iterate backwards through each RDF list
} $nil = $graph_object->{self::RDF_NIL};
foreach ($nil->usages as $usage) {
$value = self::_RDFToObject($o, $options['useNativeTypes']); $node = $usage->node;
self::addValue($node, $p, $value, array('propertyIsArray' => true)); $property = $usage->property;
$head = $usage->value;
// object may be an RDF list/partial list node but we can't know $list = [];
// easily until all triples are read $list_nodes = [];
if($object_is_id) {
if($o->value === self::RDF_NIL) { // ensure node is a well-formed list node; it must:
$object = $node_map->{$o->value}; // 1. Be referenced only once.
if(!property_exists($object, 'usages')) { // 2. Have an array for rdf:first that has 1 item.
$object->usages = array(); // 3. Have an array for rdf:rest that has 1 item.
} // 4. Have no keys other than: @id, rdf:first, rdf:rest, and,
$object->usages[] = (object)array( // optionally, @type where the value is rdf:List.
'node' => $node, $node_key_count = count(array_keys((array) $node));
'property' => $p, while ($property === self::RDF_REST &&
'value' => $value); property_exists($referenced_once, $node->{'@id'}) &&
} else if(property_exists($referenced_once, $o->value)) { is_object($referenced_once->{$node->{'@id'}}) &&
// object referenced more than once property_exists($node, self::RDF_FIRST) &&
$referenced_once->{$o->value} = false; property_exists($node, self::RDF_REST) &&
} else { is_array($node->{self::RDF_FIRST}) &&
// track single reference is_array($node->{self::RDF_REST}) &&
$referenced_once->{$o->value} = (object)array( count($node->{self::RDF_FIRST}) === 1 &&
'node' => $node, count($node->{self::RDF_REST}) === 1 &&
'property' => $p, ($node_key_count === 3 || ($node_key_count === 4 &&
'value' => $value); property_exists($node, '@type') && is_array($node->{'@type'}) &&
} count($node->{'@type'}) === 1 &&
} $node->{'@type'}[0] === self::RDF_LIST))) {
} $list[] = $node->{self::RDF_FIRST}[0];
} $list_nodes[] = $node->{'@id'};
// convert linked lists to @list arrays // get next node, moving backwards through list
foreach($graph_map as $name => $graph_object) { $usage = $referenced_once->{$node->{'@id'}};
// no @lists to be converted, continue $node = $usage->node;
if(!property_exists($graph_object, self::RDF_NIL)) { $property = $usage->property;
continue; $head = $usage->value;
} $node_key_count = count(array_keys((array) $node));
// iterate backwards through each RDF list // if node is not a blank node, then list head found
$nil = $graph_object->{self::RDF_NIL}; if (strpos($node->{'@id'}, '_:') !== 0) {
foreach($nil->usages as $usage) { break;
$node = $usage->node; }
$property = $usage->property; }
$head = $usage->value;
$list = array(); // list is nested in another list
$list_nodes = array(); if ($property === self::RDF_FIRST) {
// empty list
// ensure node is a well-formed list node; it must: if ($node->{'@id'} === self::RDF_NIL) {
// 1. Be referenced only once. // can't convert rdf:nil to a @list object because it would
// 2. Have an array for rdf:first that has 1 item. // result in a list of lists which isn't supported
// 3. Have an array for rdf:rest that has 1 item. continue;
// 4. Have no keys other than: @id, rdf:first, rdf:rest, and, }
// optionally, @type where the value is rdf:List.
$node_key_count = count(array_keys((array)$node)); // preserve list head
while($property === self::RDF_REST && $head = $graph_object->{$head->{'@id'}}->{self::RDF_REST}[0];
property_exists($referenced_once, $node->{'@id'}) && array_pop($list);
is_object($referenced_once->{$node->{'@id'}}) && array_pop($list_nodes);
property_exists($node, self::RDF_FIRST) && }
property_exists($node, self::RDF_REST) &&
is_array($node->{self::RDF_FIRST}) && // transform list into @list object
is_array($node->{self::RDF_REST}) && unset($head->{'@id'});
count($node->{self::RDF_FIRST}) === 1 && $head->{'@list'} = array_reverse($list);
count($node->{self::RDF_REST}) === 1 && foreach ($list_nodes as $list_node) {
($node_key_count === 3 || ($node_key_count === 4 && unset($graph_object->{$list_node});
property_exists($node, '@type') && is_array($node->{'@type'}) && }
count($node->{'@type'}) === 1 && }
$node->{'@type'}[0] === self::RDF_LIST))) {
$list[] = $node->{self::RDF_FIRST}[0]; unset($nil->usages);
$list_nodes[] = $node->{'@id'}; }
// get next node, moving backwards through list $result = [];
$usage = $referenced_once->{$node->{'@id'}}; $subjects = array_keys((array) $default_graph);
$node = $usage->node; sort($subjects);
$property = $usage->property; foreach ($subjects as $subject) {
$head = $usage->value; $node = $default_graph->{$subject};
$node_key_count = count(array_keys((array)$node)); if (property_exists($graph_map, $subject)) {
$node->{'@graph'} = [];
// if node is not a blank node, then list head found $graph_object = $graph_map->{$subject};
if(strpos($node->{'@id'}, '_:') !== 0) { $subjects_ = array_keys((array) $graph_object);
break; sort($subjects_);
} foreach ($subjects_ as $subject_) {
} $node_ = $graph_object->{$subject_};
// only add full subjects to top-level
// list is nested in another list if (!self::_isSubjectReference($node_)) {
if($property === self::RDF_FIRST) { $node->{'@graph'}[] = $node_;
// empty list }
if($node->{'@id'} === self::RDF_NIL) { }
// can't convert rdf:nil to a @list object because it would }
// result in a list of lists which isn't supported // only add full subjects to top-level
continue; if (!self::_isSubjectReference($node)) {
} $result[] = $node;
}
// preserve list head }
$head = $graph_object->{$head->{'@id'}}->{self::RDF_REST}[0];
array_pop($list); return $result;
array_pop($list_nodes); }
}
/**
// transform list into @list object * Processes a local context and returns a new active context.
unset($head->{'@id'}); *
$head->{'@list'} = array_reverse($list); * @param stdClass $active_ctx the current active context.
foreach($list_nodes as $list_node) { * @param mixed $local_ctx the local context to process.
unset($graph_object->{$list_node}); * @param assoc $options the context processing options.
} *
} * @return stdClass the new active context.
*/
unset($nil->usages); protected function _processContext($active_ctx, $local_ctx, $options)
} {
global $jsonld_cache;
$result = array();
$subjects = array_keys((array)$default_graph); // normalize local context to an array
sort($subjects); if (is_object($local_ctx) && property_exists($local_ctx, '@context') &&
foreach($subjects as $subject) { is_array($local_ctx->{'@context'})) {
$node = $default_graph->{$subject}; $local_ctx = $local_ctx->{'@context'};
if(property_exists($graph_map, $subject)) { }
$node->{'@graph'} = array(); $ctxs = self::arrayify($local_ctx);
$graph_object = $graph_map->{$subject};
$subjects_ = array_keys((array)$graph_object); // no contexts in array, clone existing context
sort($subjects_); if (count($ctxs) === 0) {
foreach($subjects_ as $subject_) { return self::_cloneActiveContext($active_ctx);
$node_ = $graph_object->{$subject_}; }
// only add full subjects to top-level
if(!self::_isSubjectReference($node_)) { // process each context in order, update active context
$node->{'@graph'}[] = $node_; // on each iteration to ensure proper caching
} $rval = $active_ctx;
} foreach ($ctxs as $ctx) {
} // reset to initial context
// only add full subjects to top-level if ($ctx === null) {
if(!self::_isSubjectReference($node)) { $rval = $active_ctx = $this->_getInitialContext($options);
$result[] = $node; continue;
} }
}
// dereference @context key if present
return $result; if (is_object($ctx) && property_exists($ctx, '@context')) {
} $ctx = $ctx->{'@context'};
}
/**
* Processes a local context and returns a new active context. // context must be an object by now, all URLs retrieved before this call
* if (!is_object($ctx)) {
* @param stdClass $active_ctx the current active context. throw new JsonLdException(
* @param mixed $local_ctx the local context to process. 'Invalid JSON-LD syntax; @context must be an object.', 'jsonld.SyntaxError', 'invalid local context', ['context' => $ctx]);
* @param assoc $options the context processing options. }
*
* @return stdClass the new active context. // get context from cache if available
*/ if (property_exists($jsonld_cache, 'activeCtx')) {
protected function _processContext($active_ctx, $local_ctx, $options) { $cached = $jsonld_cache->activeCtx->get($active_ctx, $ctx);
global $jsonld_cache; if ($cached) {
$rval = $active_ctx = $cached;
// normalize local context to an array $must_clone = true;
if(is_object($local_ctx) && property_exists($local_ctx, '@context') && continue;
is_array($local_ctx->{'@context'})) { }
$local_ctx = $local_ctx->{'@context'}; }
}
$ctxs = self::arrayify($local_ctx); // update active context and clone new one before updating
$active_ctx = $rval;
// no contexts in array, clone existing context $rval = self::_cloneActiveContext($rval);
if(count($ctxs) === 0) {
return self::_cloneActiveContext($active_ctx); // define context mappings for keys in local context
} $defined = new stdClass();
// process each context in order, update active context // handle @base
// on each iteration to ensure proper caching if (property_exists($ctx, '@base')) {
$rval = $active_ctx; $base = $ctx->{'@base'};
foreach($ctxs as $ctx) { if ($base === null) {
// reset to initial context $base = null;
if($ctx === null) { } else if (!is_string($base)) {
$rval = $active_ctx = $this->_getInitialContext($options); throw new JsonLdException(
continue; 'Invalid JSON-LD syntax; the value of "@base" in a ' .
} '@context must be a string or null.', 'jsonld.SyntaxError', 'invalid base IRI', ['context' => $ctx]);
} else if ($base !== '' && !self::_isAbsoluteIri($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 an absolute IRI or the empty string.', 'jsonld.SyntaxError', 'invalid base IRI', ['context' => $ctx]);
} }
if ($base !== null) {
// context must be an object by now, all URLs retrieved before this call $base = jsonld_parse_url($base);
if(!is_object($ctx)) { }
throw new JsonLdException( $rval->{'@base'} = $base;
'Invalid JSON-LD syntax; @context must be an object.', $defined->{'@base'} = true;
'jsonld.SyntaxError', 'invalid local context', }
array('context' => $ctx));
} // handle @vocab
if (property_exists($ctx, '@vocab')) {
// get context from cache if available $value = $ctx->{'@vocab'};
if(property_exists($jsonld_cache, 'activeCtx')) { if ($value === null) {
$cached = $jsonld_cache->activeCtx->get($active_ctx, $ctx); unset($rval->{'@vocab'});
if($cached) { } else if (!is_string($value)) {
$rval = $active_ctx = $cached; throw new JsonLdException(
$must_clone = true; 'Invalid JSON-LD syntax; the value of "@vocab" in a ' .
continue; '@context must be a string or null.', 'jsonld.SyntaxError', 'invalid vocab mapping', ['context' => $ctx]);
} } else if (!self::_isAbsoluteIri($value)) {
} throw new JsonLdException(
'Invalid JSON-LD syntax; the value of "@vocab" in a ' .
// update active context and clone new one before updating '@context must be an absolute IRI.', 'jsonld.SyntaxError', 'invalid vocab mapping', ['context' => $ctx]);
$active_ctx = $rval; } else {
$rval = self::_cloneActiveContext($rval); $rval->{'@vocab'} = $value;
}
// define context mappings for keys in local context $defined->{'@vocab'} = true;
$defined = new stdClass(); }
// handle @base // handle @language
if(property_exists($ctx, '@base')) { if (property_exists($ctx, '@language')) {
$base = $ctx->{'@base'}; $value = $ctx->{'@language'};
if($base === null) { if ($value === null) {
$base = null; unset($rval->{'@language'});
} else if(!is_string($base)) { } else if (!is_string($value)) {
throw new JsonLdException( throw new JsonLdException(
'Invalid JSON-LD syntax; the value of "@base" in a ' . 'Invalid JSON-LD syntax; the value of "@language" in a ' .
'@context must be a string or null.', '@context must be a string or null.', 'jsonld.SyntaxError', 'invalid default language', ['context' => $ctx]);
'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx)); } else {
} else if($base !== '' && !self::_isAbsoluteIri($base)) { $rval->{'@language'} = strtolower($value);
throw new JsonLdException( }
'Invalid JSON-LD syntax; the value of "@base" in a ' . $defined->{'@language'} = true;
'@context must be an absolute IRI or the empty string.', }
'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx));
} // process all other keys
if($base !== null) { foreach ($ctx as $k => $v) {
$base = jsonld_parse_url($base); $this->_createTermDefinition($rval, $ctx, $k, $defined);
} }
$rval->{'@base'} = $base;
$defined->{'@base'} = true; // cache result
} if (property_exists($jsonld_cache, 'activeCtx')) {
$jsonld_cache->activeCtx->set($active_ctx, $ctx, $rval);
// handle @vocab }
if(property_exists($ctx, '@vocab')) { }
$value = $ctx->{'@vocab'};
if($value === null) { return $rval;
unset($rval->{'@vocab'}); }
} else if(!is_string($value)) {
throw new JsonLdException( /**
'Invalid JSON-LD syntax; the value of "@vocab" in a ' . * Expands a language map.
'@context must be a string or null.', *
'jsonld.SyntaxError', 'invalid vocab mapping', * @param stdClass $language_map the language map to expand.
array('context' => $ctx)); *
} else if(!self::_isAbsoluteIri($value)) { * @return array the expanded language map.
throw new JsonLdException( */
'Invalid JSON-LD syntax; the value of "@vocab" in a ' . protected function _expandLanguageMap($language_map)
'@context must be an absolute IRI.', {
'jsonld.SyntaxError', 'invalid vocab mapping', $rval = [];
array('context' => $ctx)); $keys = array_keys((array) $language_map);
} else { sort($keys);
$rval->{'@vocab'} = $value; foreach ($keys as $key) {
} $values = $language_map->{$key};
$defined->{'@vocab'} = true; $values = self::arrayify($values);
} foreach ($values as $item) {
if ($item === null) {
// handle @language continue;
if(property_exists($ctx, '@language')) { }
$value = $ctx->{'@language'}; if (!is_string($item)) {
if($value === null) { throw new JsonLdException(
unset($rval->{'@language'}); 'Invalid JSON-LD syntax; language map values must be strings.', 'jsonld.SyntaxError', 'invalid language map value', ['languageMap', $language_map]);
} else if(!is_string($value)) { }
throw new JsonLdException( $rval[] = (object) [
'Invalid JSON-LD syntax; the value of "@language" in a ' . '@value' => $item,
'@context must be a string or null.', '@language' => strtolower($key)];
'jsonld.SyntaxError', 'invalid default language', }
array('context' => $ctx)); }
} else { return $rval;
$rval->{'@language'} = strtolower($value); }
}
$defined->{'@language'} = true; /**
} * Labels the blank nodes in the given value using the given UniqueNamer.
*
// process all other keys * @param UniqueNamer $namer the UniqueNamer to use.
foreach($ctx as $k => $v) { * @param mixed $element the element with blank nodes to rename.
$this->_createTermDefinition($rval, $ctx, $k, $defined); *
} * @return mixed the element.
*/
// cache result public function _labelBlankNodes($namer, $element)
if(property_exists($jsonld_cache, 'activeCtx')) { {
$jsonld_cache->activeCtx->set($active_ctx, $ctx, $rval); if (is_array($element)) {
} $length = count($element);
} for ($i = 0; $i < $length; ++$i) {
$element[$i] = $this->_labelBlankNodes($namer, $element[$i]);
return $rval; }
} } else if (self::_isList($element)) {
$element->{'@list'} = $this->_labelBlankNodes(
/** $namer, $element->{'@list'});
* Expands a language map. } else if (is_object($element)) {
* // rename blank node
* @param stdClass $language_map the language map to expand. if (self::_isBlankNode($element)) {
* $name = null;
* @return array the expanded language map. if (property_exists($element, '@id')) {
*/ $name = $element->{'@id'};
protected function _expandLanguageMap($language_map) { }
$rval = array(); $element->{'@id'} = $namer->getName($name);
$keys = array_keys((array)$language_map); }
sort($keys);
foreach($keys as $key) { // recursively apply to all keys
$values = $language_map->{$key}; $keys = array_keys((array) $element);
$values = self::arrayify($values); sort($keys);
foreach($values as $item) { foreach ($keys as $key) {
if($item === null) { if ($key !== '@id') {
continue; $element->{$key} = $this->_labelBlankNodes($namer, $element->{$key});
} }
if(!is_string($item)) { }
throw new JsonLdException( }
'Invalid JSON-LD syntax; language map values must be strings.',
'jsonld.SyntaxError', 'invalid language map value', return $element;
array('languageMap', $language_map)); }
}
$rval[] = (object)array( /**
'@value' => $item, * Expands the given value by using the coercion and keyword rules in the
'@language' => strtolower($key)); * given context.
} *
} * @param stdClass $active_ctx the active context to use.
return $rval; * @param string $active_property the property the value is associated with.
} * @param mixed $value the value to expand.
*
/** * @return mixed the expanded value.
* Labels the blank nodes in the given value using the given UniqueNamer. */
* protected function _expandValue($active_ctx, $active_property, $value)
* @param UniqueNamer $namer the UniqueNamer to use. {
* @param mixed $element the element with blank nodes to rename. // nothing to expand
* if ($value === null) {
* @return mixed the element. return null;
*/ }
public function _labelBlankNodes($namer, $element) {
if(is_array($element)) { // special-case expand @id and @type (skips '@id' expansion)
$length = count($element); $expanded_property = $this->_expandIri(
for($i = 0; $i < $length; ++$i) { $active_ctx, $active_property, ['vocab' => true]);
$element[$i] = $this->_labelBlankNodes($namer, $element[$i]); if ($expanded_property === '@id') {
} return $this->_expandIri($active_ctx, $value, ['base' => true]);
} else if(self::_isList($element)) { } else if ($expanded_property === '@type') {
$element->{'@list'} = $this->_labelBlankNodes( return $this->_expandIri(
$namer, $element->{'@list'}); $active_ctx, $value, ['vocab' => true, 'base' => true]);
} else if(is_object($element)) { }
// rename blank node
if(self::_isBlankNode($element)) { // get type definition from context
$name = null; $type = self::getContextValue($active_ctx, $active_property, '@type');
if(property_exists($element, '@id')) {
$name = $element->{'@id'}; // do @id expansion (automatic for @graph)
} if ($type === '@id' || ($expanded_property === '@graph' &&
$element->{'@id'} = $namer->getName($name); is_string($value))) {
} return (object) ['@id' => $this->_expandIri(
$active_ctx, $value, ['base' => true])];
// recursively apply to all keys }
$keys = array_keys((array)$element); // do @id expansion w/vocab
sort($keys); if ($type === '@vocab') {
foreach($keys as $key) { return (object) ['@id' => $this->_expandIri(
if($key !== '@id') { $active_ctx, $value, ['vocab' => true, 'base' => true])];
$element->{$key} = $this->_labelBlankNodes($namer, $element->{$key}); }
}
} // do not expand keyword values
} if (self::_isKeyword($expanded_property)) {
return $value;
return $element; }
}
$rval = new stdClass();
/**
* Expands the given value by using the coercion and keyword rules in the // other type
* given context. if ($type !== null) {
* $rval->{'@type'} = $type;
* @param stdClass $active_ctx the active context to use. } else if (is_string($value)) {
* @param string $active_property the property the value is associated with. // check for language tagging for strings
* @param mixed $value the value to expand. $language = self::getContextValue(
* $active_ctx, $active_property, '@language');
* @return mixed the expanded value. if ($language !== null) {
*/ $rval->{'@language'} = $language;
protected function _expandValue($active_ctx, $active_property, $value) { }
// nothing to expand }
if($value === null) { $rval->{'@value'} = $value;
return null;
} return $rval;
}
// special-case expand @id and @type (skips '@id' expansion)
$expanded_property = $this->_expandIri( /**
$active_ctx, $active_property, array('vocab' => true)); * Creates an array of RDF triples for the given graph.
if($expanded_property === '@id') { *
return $this->_expandIri($active_ctx, $value, array('base' => true)); * @param stdClass $graph the graph to create RDF triples for.
} else if($expanded_property === '@type') { * @param UniqueNamer $namer for assigning bnode names.
return $this->_expandIri( * @param assoc $options the RDF serialization options.
$active_ctx, $value, array('vocab' => true, 'base' => true)); *
} * @return array the array of RDF triples for the given graph.
*/
// get type definition from context protected function _graphToRDF($graph, $namer, $options)
$type = self::getContextValue($active_ctx, $active_property, '@type'); {
$rval = [];
// do @id expansion (automatic for @graph)
if($type === '@id' || ($expanded_property === '@graph' && $ids = array_keys((array) $graph);
is_string($value))) { sort($ids);
return (object)array('@id' => $this->_expandIri( foreach ($ids as $id) {
$active_ctx, $value, array('base' => true))); $node = $graph->{$id};
} if ($id === '"') {
// do @id expansion w/vocab $id = '';
if($type === '@vocab') { }
return (object)array('@id' => $this->_expandIri( $properties = array_keys((array) $node);
$active_ctx, $value, array('vocab' => true, 'base' => true))); sort($properties);
} foreach ($properties as $property) {
$items = $node->{$property};
// do not expand keyword values if ($property === '@type') {
if(self::_isKeyword($expanded_property)) { $property = self::RDF_TYPE;
return $value; } else if (self::_isKeyword($property)) {
} continue;
}
$rval = new stdClass();
foreach ($items as $item) {
// other type // skip relative IRI subjects and predicates
if($type !== null) { if (!(self::_isAbsoluteIri($id) && self::_isAbsoluteIri($property))) {
$rval->{'@type'} = $type; continue;
} else if(is_string($value)) { }
// check for language tagging for strings
$language = self::getContextValue( // RDF subject
$active_ctx, $active_property, '@language'); $subject = new stdClass();
if($language !== null) { $subject->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI';
$rval->{'@language'} = $language; $subject->value = $id;
}
} // RDF predicate
$rval->{'@value'} = $value; $predicate = new stdClass();
$predicate->type = (strpos($property, '_:') === 0 ?
return $rval; 'blank node' : 'IRI');
} $predicate->value = $property;
/** // skip bnode predicates unless producing generalized RDF
* Creates an array of RDF triples for the given graph. if ($predicate->type === 'blank node' &&
* !$options['produceGeneralizedRdf']) {
* @param stdClass $graph the graph to create RDF triples for. continue;
* @param UniqueNamer $namer for assigning bnode names. }
* @param assoc $options the RDF serialization options.
* if (self::_isList($item)) {
* @return array the array of RDF triples for the given graph. // convert @list to triples
*/ $this->_listToRDF(
protected function _graphToRDF($graph, $namer, $options) { $item->{'@list'}, $namer, $subject, $predicate, $rval);
$rval = array(); } else {
// convert value or node object to triple
$ids = array_keys((array)$graph); $object = $this->_objectToRDF($item);
sort($ids); // skip null objects (they are relative IRIs)
foreach($ids as $id) { if ($object) {
$node = $graph->{$id}; $rval[] = (object) [
if($id === '"') { 'subject' => $subject,
$id = ''; 'predicate' => $predicate,
} 'object' => $object];
$properties = array_keys((array)$node); }
sort($properties); }
foreach($properties as $property) { }
$items = $node->{$property}; }
if($property === '@type') { }
$property = self::RDF_TYPE;
} else if(self::_isKeyword($property)) { return $rval;
continue; }
}
/**
foreach($items as $item) { * Converts a @list value into linked list of blank node RDF triples
// skip relative IRI subjects and predicates * (an RDF collection).
if(!(self::_isAbsoluteIri($id) && self::_isAbsoluteIri($property))) { *
continue; * @param array $list the @list value.
} * @param UniqueNamer $namer for assigning blank node names.
* @param stdClass $subject the subject for the head of the list.
// RDF subject * @param stdClass $predicate the predicate for the head of the list.
$subject = new stdClass(); * @param &array $triples the array of triples to append to.
$subject->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; */
$subject->value = $id; protected function _listToRDF(
$list, $namer, $subject, $predicate, &$triples)
// RDF predicate {
$predicate = new stdClass(); $first = (object) ['type' => 'IRI', 'value' => self::RDF_FIRST];
$predicate->type = (strpos($property, '_:') === 0 ? $rest = (object) ['type' => 'IRI', 'value' => self::RDF_REST];
'blank node' : 'IRI'); $nil = (object) ['type' => 'IRI', 'value' => self::RDF_NIL];
$predicate->value = $property;
foreach ($list as $item) {
// skip bnode predicates unless producing generalized RDF $blank_node = (object) [
if($predicate->type === 'blank node' && 'type' => 'blank node', 'value' => $namer->getName()];
!$options['produceGeneralizedRdf']) { $triples[] = (object) [
continue; 'subject' => $subject,
} 'predicate' => $predicate,
'object' => $blank_node];
if(self::_isList($item)) {
// convert @list to triples $subject = $blank_node;
$this->_listToRDF( $predicate = $first;
$item->{'@list'}, $namer, $subject, $predicate, $rval); $object = $this->_objectToRDF($item);
} else { // skip null objects (they are relative IRIs)
// convert value or node object to triple if ($object) {
$object = $this->_objectToRDF($item); $triples[] = (object) [
// skip null objects (they are relative IRIs) 'subject' => $subject,
if($object) { 'predicate' => $predicate,
$rval[] = (object)array( 'object' => $object];
'subject' => $subject, }
'predicate' => $predicate,
'object' => $object); $predicate = $rest;
} }
}
} $triples[] = (object) [
} 'subject' => $subject, 'predicate' => $predicate, 'object' => $nil];
} }
return $rval; /**
} * Converts a JSON-LD value object to an RDF literal or a JSON-LD string or
* node object to an RDF resource.
/** *
* Converts a @list value into linked list of blank node RDF triples * @param mixed $item the JSON-LD value or node object.
* (an RDF collection). *
* * @return stdClass the RDF literal or RDF resource.
* @param array $list the @list value. */
* @param UniqueNamer $namer for assigning blank node names. protected function _objectToRDF($item)
* @param stdClass $subject the subject for the head of the list. {
* @param stdClass $predicate the predicate for the head of the list. $object = new stdClass();
* @param &array $triples the array of triples to append to.
*/ if (self::_isValue($item)) {
protected function _listToRDF( $object->type = 'literal';
$list, $namer, $subject, $predicate, &$triples) { $value = $item->{'@value'};
$first = (object)array('type' => 'IRI', 'value' => self::RDF_FIRST); $datatype = property_exists($item, '@type') ? $item->{'@type'} : null;
$rest = (object)array('type' => 'IRI', 'value' => self::RDF_REST);
$nil = (object)array('type' => 'IRI', 'value' => self::RDF_NIL); // convert to XSD datatypes as appropriate
if (is_bool($value)) {
foreach($list as $item) { $object->value = ($value ? 'true' : 'false');
$blank_node = (object)array( $object->datatype = $datatype ? $datatype : self::XSD_BOOLEAN;
'type' => 'blank node', 'value' => $namer->getName()); } else if (is_double($value) || $datatype == self::XSD_DOUBLE) {
$triples[] = (object)array( // canonical double representation
'subject' => $subject, $object->value = preg_replace(
'predicate' => $predicate, '/(\d)0*E\+?/', '$1E', sprintf('%1.15E', $value));
'object' => $blank_node); $object->datatype = $datatype ? $datatype : self::XSD_DOUBLE;
} else if (is_integer($value)) {
$subject = $blank_node; $object->value = strval($value);
$predicate = $first; $object->datatype = $datatype ? $datatype : self::XSD_INTEGER;
$object = $this->_objectToRDF($item); } else if (property_exists($item, '@language')) {
// skip null objects (they are relative IRIs) $object->value = $value;
if($object) { $object->datatype = $datatype ? $datatype : self::RDF_LANGSTRING;
$triples[] = (object)array( $object->language = $item->{'@language'};
'subject' => $subject, } else {
'predicate' => $predicate, $object->value = $value;
'object' => $object); $object->datatype = $datatype ? $datatype : self::XSD_STRING;
} }
} else {
$predicate = $rest; // convert string/node object to RDF
} $id = is_object($item) ? $item->{'@id'} : $item;
$object->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI';
$triples[] = (object)array( $object->value = $id;
'subject' => $subject, 'predicate' => $predicate, 'object' => $nil); }
}
// skip relative IRIs
/** if ($object->type === 'IRI' && !self::_isAbsoluteIri($object->value)) {
* Converts a JSON-LD value object to an RDF literal or a JSON-LD string or return null;
* node object to an RDF resource. }
*
* @param mixed $item the JSON-LD value or node object. return $object;
* }
* @return stdClass the RDF literal or RDF resource.
*/ /**
protected function _objectToRDF($item) { * Converts an RDF triple object to a JSON-LD object.
$object = new stdClass(); *
* @param stdClass $o the RDF triple object to convert.
if(self::_isValue($item)) { * @param bool $use_native_types true to output native types, false not to.
$object->type = 'literal'; *
$value = $item->{'@value'}; * @return stdClass the JSON-LD object.
$datatype = property_exists($item, '@type') ? $item->{'@type'} : null; */
protected function _RDFToObject($o, $use_native_types)
// convert to XSD datatypes as appropriate {
if(is_bool($value)) { // convert IRI/blank node object to JSON-LD
$object->value = ($value ? 'true' : 'false'); if ($o->type === 'IRI' || $o->type === 'blank node') {
$object->datatype = $datatype ? $datatype : self::XSD_BOOLEAN; return (object) ['@id' => $o->value];
} else if(is_double($value) || $datatype == self::XSD_DOUBLE) { }
// canonical double representation
$object->value = preg_replace( // convert literal object to JSON-LD
'/(\d)0*E\+?/', '$1E', sprintf('%1.15E', $value)); $rval = (object) ['@value' => $o->value];
$object->datatype = $datatype ? $datatype : self::XSD_DOUBLE;
} else if(is_integer($value)) { if (property_exists($o, 'language')) {
$object->value = strval($value); // add language
$object->datatype = $datatype ? $datatype : self::XSD_INTEGER; $rval->{'@language'} = $o->language;
} else if(property_exists($item, '@language')) { } else {
$object->value = $value; // add datatype
$object->datatype = $datatype ? $datatype : self::RDF_LANGSTRING; $type = $o->datatype;
$object->language = $item->{'@language'}; // use native types for certain xsd types
} else { if ($use_native_types) {
$object->value = $value; if ($type === self::XSD_BOOLEAN) {
$object->datatype = $datatype ? $datatype : self::XSD_STRING; if ($rval->{'@value'} === 'true') {
} $rval->{'@value'} = true;
} else { } else if ($rval->{'@value'} === 'false') {
// convert string/node object to RDF $rval->{'@value'} = false;
$id = is_object($item) ? $item->{'@id'} : $item; }
$object->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; } else if (is_numeric($rval->{'@value'})) {
$object->value = $id; if ($type === self::XSD_INTEGER) {
} $i = intval($rval->{'@value'});
if (strval($i) === $rval->{'@value'}) {
// skip relative IRIs $rval->{'@value'} = $i;
if($object->type === 'IRI' && !self::_isAbsoluteIri($object->value)) { }
return null; } else if ($type === self::XSD_DOUBLE) {
} $rval->{'@value'} = doubleval($rval->{'@value'});
}
return $object; }
} // do not add native type
if (!in_array($type, [
/** self::XSD_BOOLEAN, self::XSD_INTEGER, self::XSD_DOUBLE,
* Converts an RDF triple object to a JSON-LD object. self::XSD_STRING])) {
* $rval->{'@type'} = $type;
* @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_STRING) {
* $rval->{'@type'} = $type;
* @return stdClass the JSON-LD object. }
*/ }
protected function _RDFToObject($o, $use_native_types) {
// convert IRI/blank node object to JSON-LD return $rval;
if($o->type === 'IRI' || $o->type === 'blank node') { }
return (object)array('@id' => $o->value);
} /**
* Recursively flattens the subjects in the given JSON-LD expanded input
// convert literal object to JSON-LD * into a node map.
$rval = (object)array('@value' => $o->value); *
* @param mixed $input the JSON-LD expanded input.
if(property_exists($o, 'language')) { * @param stdClass $graphs a map of graph name to subject map.
// add language * @param string $graph the name of the current graph.
$rval->{'@language'} = $o->language; * @param UniqueNamer $namer the blank node namer.
} else { * @param mixed $name the name assigned to the current input if it is a bnode.
// add datatype * @param mixed $list the list to append to, null for none.
$type = $o->datatype; */
// use native types for certain xsd types protected function _createNodeMap(
if($use_native_types) { $input, $graphs, $graph, $namer, $name = null, $list = null)
if($type === self::XSD_BOOLEAN) { {
if($rval->{'@value'} === 'true') { // recurse through array
$rval->{'@value'} = true; if (is_array($input)) {
} else if($rval->{'@value'} === 'false') { foreach ($input as $e) {
$rval->{'@value'} = false; $this->_createNodeMap($e, $graphs, $graph, $namer, null, $list);
} }
} else if(is_numeric($rval->{'@value'})) { return;
if($type === self::XSD_INTEGER) { }
$i = intval($rval->{'@value'});
if(strval($i) === $rval->{'@value'}) { // add non-object to list
$rval->{'@value'} = $i; if (!is_object($input)) {
} if ($list !== null) {
} else if($type === self::XSD_DOUBLE) { $list[] = $input;
$rval->{'@value'} = doubleval($rval->{'@value'}); }
} return;
} }
// do not add native type
if(!in_array($type, array( // add values to list
self::XSD_BOOLEAN, self::XSD_INTEGER, self::XSD_DOUBLE, if (self::_isValue($input)) {
self::XSD_STRING))) { if (property_exists($input, '@type')) {
$rval->{'@type'} = $type; $type = $input->{'@type'};
} // rename @type blank node
} else if($type !== self::XSD_STRING) { if (strpos($type, '_:') === 0) {
$rval->{'@type'} = $type; $type = $input->{'@type'} = $namer->getName($type);
} }
} }
if ($list !== null) {
return $rval; $list[] = $input;
} }
return;
/** }
* Recursively flattens the subjects in the given JSON-LD expanded input
* into a node map. // Note: At this point, input must be a subject.
* // spec requires @type to be named first, so assign names early
* @param mixed $input the JSON-LD expanded input. if (property_exists($input, '@type')) {
* @param stdClass $graphs a map of graph name to subject map. foreach ($input->{'@type'} as $type) {
* @param string $graph the name of the current graph. if (strpos($type, '_:') === 0) {
* @param UniqueNamer $namer the blank node namer. $namer->getName($type);
* @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. }
*/ }
protected function _createNodeMap(
$input, $graphs, $graph, $namer, $name=null, $list=null) { // get name for subject
// recurse through array if ($name === null) {
if(is_array($input)) { if (property_exists($input, '@id')) {
foreach($input as $e) { $name = $input->{'@id'};
$this->_createNodeMap($e, $graphs, $graph, $namer, null, $list); }
} if (self::_isBlankNode($input)) {
return; $name = $namer->getName($name);
} }
}
// add non-object to list
if(!is_object($input)) { // add subject reference to list
if($list !== null) { if ($list !== null) {
$list[] = $input; $list[] = (object) ['@id' => $name];
} }
return;
} // create new subject or merge into existing one
if (!property_exists($graphs, $graph)) {
// add values to list $graphs->{$graph} = new stdClass();
if(self::_isValue($input)) { }
if(property_exists($input, '@type')) { $subjects = $graphs->{$graph};
$type = $input->{'@type'}; if (!property_exists($subjects, $name)) {
// rename @type blank node if ($name === '') {
if(strpos($type, '_:') === 0) { $subjects->{'"'} = new stdClass();
$type = $input->{'@type'} = $namer->getName($type); } else {
} $subjects->{$name} = new stdClass();
} }
if($list !== null) { }
$list[] = $input; if ($name === '') {
} $subject = $subjects->{'"'};
return; } else {
} $subject = $subjects->{$name};
}
// Note: At this point, input must be a subject. $subject->{'@id'} = $name;
$properties = array_keys((array) $input);
// spec requires @type to be named first, so assign names early sort($properties);
if(property_exists($input, '@type')) { foreach ($properties as $property) {
foreach($input->{'@type'} as $type) { // skip @id
if(strpos($type, '_:') === 0) { if ($property === '@id') {
$namer->getName($type); continue;
} }
}
} // handle reverse properties
if ($property === '@reverse') {
// get name for subject $referenced_node = (object) ['@id' => $name];
if($name === null) { $reverse_map = $input->{'@reverse'};
if(property_exists($input, '@id')) { foreach ($reverse_map as $reverse_property => $items) {
$name = $input->{'@id'}; foreach ($items as $item) {
} $item_name = null;
if(self::_isBlankNode($input)) { if (property_exists($item, '@id')) {
$name = $namer->getName($name); $item_name = $item->{'@id'};
} }
} if (self::_isBlankNode($item)) {
$item_name = $namer->getName($item_name);
// add subject reference to list }
if($list !== null) { $this->_createNodeMap($item, $graphs, $graph, $namer, $item_name);
$list[] = (object)array('@id' => $name); if ($item_name === '') {
} $item_name = '"';
}
// create new subject or merge into existing one self::addValue(
if(!property_exists($graphs, $graph)) { $subjects->{$item_name}, $reverse_property, $referenced_node, ['propertyIsArray' => true, 'allowDuplicate' => false]);
$graphs->{$graph} = new stdClass(); }
} }
$subjects = $graphs->{$graph}; continue;
if(!property_exists($subjects, $name)) { }
if($name === '') {
$subjects->{'"'} = new stdClass(); // recurse into graph
} else { if ($property === '@graph') {
$subjects->{$name} = new stdClass(); // add graph subjects map entry
} if (!property_exists($graphs, $name)) {
} // FIXME: temporary hack to avoid empty property bug
if($name === '') { if (!$name) {
$subject = $subjects->{'"'}; $name = '"';
} else { }
$subject = $subjects->{$name}; $graphs->{$name} = new stdClass();
} }
$subject->{'@id'} = $name; $g = ($graph === '@merged') ? $graph : $name;
$properties = array_keys((array)$input); $this->_createNodeMap(
sort($properties); $input->{$property}, $graphs, $g, $namer, null, null);
foreach($properties as $property) { continue;
// skip @id }
if($property === '@id') {
continue; // copy non-@type keywords
} if ($property !== '@type' && self::_isKeyword($property)) {
if ($property === '@index' && property_exists($subject, '@index') &&
// handle reverse properties ($input->{'@index'} !== $subject->{'@index'} ||
if($property === '@reverse') { $input->{'@index'}->{'@id'} !== $subject->{'@index'}->{'@id'})) {
$referenced_node = (object)array('@id' => $name); throw new JsonLdException(
$reverse_map = $input->{'@reverse'}; 'Invalid JSON-LD syntax; conflicting @index property detected.', 'jsonld.SyntaxError', 'conflicting indexes', ['subject' => $subject]);
foreach($reverse_map as $reverse_property => $items) { }
foreach($items as $item) { $subject->{$property} = $input->{$property};
$item_name = null; continue;
if(property_exists($item, '@id')) { }
$item_name = $item->{'@id'};
} // iterate over objects
if(self::_isBlankNode($item)) { $objects = $input->{$property};
$item_name = $namer->getName($item_name);
} // if property is a bnode, assign it a new id
$this->_createNodeMap($item, $graphs, $graph, $namer, $item_name); if (strpos($property, '_:') === 0) {
if($item_name === '') { $property = $namer->getName($property);
$item_name = '"'; }
}
self::addValue( // ensure property is added for empty arrays
$subjects->{$item_name}, $reverse_property, $referenced_node, if (count($objects) === 0) {
array('propertyIsArray' => true, 'allowDuplicate' => false)); self::addValue(
} $subject, $property, [], ['propertyIsArray' => true]);
} continue;
continue; }
} foreach ($objects as $o) {
if ($property === '@type') {
// recurse into graph // rename @type blank nodes
if($property === '@graph') { $o = (strpos($o, '_:') === 0) ? $namer->getName($o) : $o;
// add graph subjects map entry }
if(!property_exists($graphs, $name)) {
// FIXME: temporary hack to avoid empty property bug // handle embedded subject or subject reference
if(!$name) { if (self::_isSubject($o) || self::_isSubjectReference($o)) {
$name = '"'; // rename blank node @id
} $id = property_exists($o, '@id') ? $o->{'@id'} : null;
$graphs->{$name} = new stdClass(); if (self::_isBlankNode($o)) {
} $id = $namer->getName($id);
$g = ($graph === '@merged') ? $graph : $name; }
$this->_createNodeMap(
$input->{$property}, $graphs, $g, $namer, null, null); // add reference and recurse
continue; self::addValue(
} $subject, $property, (object) ['@id' => $id], ['propertyIsArray' => true, 'allowDuplicate' => false]);
$this->_createNodeMap($o, $graphs, $graph, $namer, $id, null);
// copy non-@type keywords } else if (self::_isList($o)) {
if($property !== '@type' && self::_isKeyword($property)) { // handle @list
if($property === '@index' && property_exists($subject, '@index') && $_list = new ArrayObject();
($input->{'@index'} !== $subject->{'@index'} || $this->_createNodeMap(
$input->{'@index'}->{'@id'} !== $subject->{'@index'}->{'@id'})) { $o->{'@list'}, $graphs, $graph, $namer, $name, $_list);
throw new JsonLdException( $o = (object) ['@list' => (array) $_list];
'Invalid JSON-LD syntax; conflicting @index property detected.', self::addValue(
'jsonld.SyntaxError', 'conflicting indexes', $subject, $property, $o, ['propertyIsArray' => true, 'allowDuplicate' => false]);
array('subject' => $subject)); } else {
} // handle @value
$subject->{$property} = $input->{$property}; $this->_createNodeMap($o, $graphs, $graph, $namer, $name, null);
continue; self::addValue(
} $subject, $property, $o, ['propertyIsArray' => true, 'allowDuplicate' => false]);
}
// iterate over objects }
$objects = $input->{$property}; }
}
// if property is a bnode, assign it a new id
if(strpos($property, '_:') === 0) { /**
$property = $namer->getName($property); * Frames subjects according to the given frame.
} *
* @param stdClass $state the current framing state.
// ensure property is added for empty arrays * @param array $subjects the subjects to filter.
if(count($objects) === 0) { * @param array $frame the frame.
self::addValue( * @param mixed $parent the parent subject or top-level array.
$subject, $property, array(), array('propertyIsArray' => true)); * @param mixed $property the parent property, initialized to null.
continue; */
} protected function _matchFrame(
foreach($objects as $o) { $state, $subjects, $frame, $parent, $property)
if($property === '@type') { {
// rename @type blank nodes // validate the frame
$o = (strpos($o, '_:') === 0) ? $namer->getName($o) : $o; $this->_validateFrame($frame);
} $frame = $frame[0];
// handle embedded subject or subject reference // get flags for current frame
if(self::_isSubject($o) || self::_isSubjectReference($o)) { $options = $state->options;
// rename blank node @id $flags = [
$id = property_exists($o, '@id') ? $o->{'@id'} : null; 'embed' => $this->_getFrameFlag($frame, $options, 'embed'),
if(self::_isBlankNode($o)) { 'explicit' => $this->_getFrameFlag($frame, $options, 'explicit'),
$id = $namer->getName($id); 'requireAll' => $this->_getFrameFlag($frame, $options, 'requireAll')];
}
// filter out subjects that match the frame
// add reference and recurse $matches = $this->_filterSubjects($state, $subjects, $frame, $flags);
self::addValue(
$subject, $property, (object)array('@id' => $id), // add matches to output
array('propertyIsArray' => true, 'allowDuplicate' => false)); foreach ($matches as $id => $subject) {
$this->_createNodeMap($o, $graphs, $graph, $namer, $id, null); if ($flags['embed'] === '@link' && property_exists($state->link, $id)) {
} else if(self::_isList($o)) { // TODO: may want to also match an existing linked subject against
// handle @list // the current frame ... so different frames could produce different
$_list = new ArrayObject(); // subjects that are only shared in-memory when the frames are the same
$this->_createNodeMap( // add existing linked subject
$o->{'@list'}, $graphs, $graph, $namer, $name, $_list); $this->_addFrameOutput($parent, $property, $state->link->{$id});
$o = (object)array('@list' => (array)$_list); continue;
self::addValue( }
$subject, $property, $o,
array('propertyIsArray' => true, 'allowDuplicate' => false)); /* Note: In order to treat each top-level match as a compartmentalized
} else { result, clear the unique embedded subjects map when the property is null,
// handle @value which only occurs at the top-level. */
$this->_createNodeMap($o, $graphs, $graph, $namer, $name, null); if ($property === null) {
self::addValue( $state->uniqueEmbeds = new stdClass();
$subject, $property, $o, }
array('propertyIsArray' => true, 'allowDuplicate' => false));
} // start output for subject
} $output = new stdClass();
} $output->{'@id'} = $id;
} $state->link->{$id} = $output;
/** // if embed is @never or if a circular reference would be created by an
* Frames subjects according to the given frame. // embed, the subject cannot be embedded, just add the reference;
* // note that a circular reference won't occur when the embed flag is
* @param stdClass $state the current framing state. // `@link` as the above check will short-circuit before reaching this point
* @param array $subjects the subjects to filter. if ($flags['embed'] === '@never' ||
* @param array $frame the frame. $this->_createsCircularReference($subject, $state->subjectStack)) {
* @param mixed $parent the parent subject or top-level array. $this->_addFrameOutput($parent, $property, $output);
* @param mixed $property the parent property, initialized to null. continue;
*/ }
protected function _matchFrame(
$state, $subjects, $frame, $parent, $property) { // if only the last match should be embedded
// validate the frame if ($flags['embed'] === '@last') {
$this->_validateFrame($frame); // remove any existing embed
$frame = $frame[0]; if (property_exists($state->uniqueEmbeds, $id)) {
$this->_removeEmbed($state, $id);
// get flags for current frame }
$options = $state->options; $state->uniqueEmbeds->{$id} = [
$flags = array( 'parent' => $parent, 'property' => $property];
'embed' => $this->_getFrameFlag($frame, $options, 'embed'), }
'explicit' => $this->_getFrameFlag($frame, $options, 'explicit'),
'requireAll' => $this->_getFrameFlag($frame, $options, 'requireAll')); // push matching subject onto stack to enable circular embed checks
$state->subjectStack[] = $subject;
// filter out subjects that match the frame
$matches = $this->_filterSubjects($state, $subjects, $frame, $flags); // iterate over subject properties
$props = array_keys((array) $subject);
// add matches to output sort($props);
foreach($matches as $id => $subject) { foreach ($props as $prop) {
if($flags['embed'] === '@link' && property_exists($state->link, $id)) { // copy keywords to output
// TODO: may want to also match an existing linked subject against if (self::_isKeyword($prop)) {
// the current frame ... so different frames could produce different $output->{$prop} = self::copy($subject->{$prop});
// subjects that are only shared in-memory when the frames are the same continue;
}
// add existing linked subject
$this->_addFrameOutput($parent, $property, $state->link->{$id}); // explicit is on and property isn't in the frame, skip processing
continue; if ($flags['explicit'] && !property_exists($frame, $prop)) {
} continue;
}
/* Note: In order to treat each top-level match as a compartmentalized
result, clear the unique embedded subjects map when the property is null, // add objects
which only occurs at the top-level. */ $objects = $subject->{$prop};
if($property === null) { foreach ($objects as $o) {
$state->uniqueEmbeds = new stdClass(); // recurse into list
} if (self::_isList($o)) {
// add empty list
// start output for subject $list = (object) ['@list' => []];
$output = new stdClass(); $this->_addFrameOutput($output, $prop, $list);
$output->{'@id'} = $id;
$state->link->{$id} = $output; // add list objects
$src = $o->{'@list'};
// if embed is @never or if a circular reference would be created by an foreach ($src as $o) {
// embed, the subject cannot be embedded, just add the reference; if (self::_isSubjectReference($o)) {
// note that a circular reference won't occur when the embed flag is // recurse into subject reference
// `@link` as the above check will short-circuit before reaching this point $subframe = (property_exists($frame, $prop) ?
if($flags['embed'] === '@never' || $frame->{$prop}[0]->{'@list'} :
$this->_createsCircularReference($subject, $state->subjectStack)) { $this->_createImplicitFrame($flags));
$this->_addFrameOutput($parent, $property, $output); $this->_matchFrame(
continue; $state, [$o->{'@id'}], $subframe, $list, '@list');
} } else {
// include other values automatically
// if only the last match should be embedded $this->_addFrameOutput($list, '@list', self::copy($o));
if($flags['embed'] === '@last') { }
// remove any existing embed }
if(property_exists($state->uniqueEmbeds, $id)) { continue;
$this->_removeEmbed($state, $id); }
}
$state->uniqueEmbeds->{$id} = array( if (self::_isSubjectReference($o)) {
'parent' => $parent, 'property' => $property); // recurse into subject reference
} $subframe = (property_exists($frame, $prop) ?
$frame->{$prop} : $this->_createImplicitFrame($flags));
// push matching subject onto stack to enable circular embed checks $this->_matchFrame(
$state->subjectStack[] = $subject; $state, [$o->{'@id'}], $subframe, $output, $prop);
} else {
// iterate over subject properties // include other values automatically
$props = array_keys((array)$subject); $this->_addFrameOutput($output, $prop, self::copy($o));
sort($props); }
foreach($props as $prop) { }
// copy keywords to output }
if(self::_isKeyword($prop)) {
$output->{$prop} = self::copy($subject->{$prop}); // handle defaults
continue; $props = array_keys((array) $frame);
} sort($props);
foreach ($props as $prop) {
// explicit is on and property isn't in the frame, skip processing // skip keywords
if($flags['explicit'] && !property_exists($frame, $prop)) { if (self::_isKeyword($prop)) {
continue; continue;
} }
// add objects // if omit default is off, then include default values for properties
$objects = $subject->{$prop}; // that appear in the next frame but are not in the matching subject
foreach($objects as $o) { $next = $frame->{$prop}[0];
// recurse into list $omit_default_on = $this->_getFrameFlag(
if(self::_isList($o)) { $next, $options, 'omitDefault');
// add empty list if (!$omit_default_on && !property_exists($output, $prop)) {
$list = (object)array('@list' => array()); $preserve = '@null';
$this->_addFrameOutput($output, $prop, $list); if (property_exists($next, '@default')) {
$preserve = self::copy($next->{'@default'});
// add list objects }
$src = $o->{'@list'}; $preserve = self::arrayify($preserve);
foreach($src as $o) { $output->{$prop} = [(object) ['@preserve' => $preserve]];
if(self::_isSubjectReference($o)) { }
// recurse into subject reference }
$subframe = (property_exists($frame, $prop) ?
$frame->{$prop}[0]->{'@list'} : // add output to parent
$this->_createImplicitFrame($flags)); $this->_addFrameOutput($parent, $property, $output);
$this->_matchFrame(
$state, array($o->{'@id'}), $subframe, $list, '@list'); // pop matching subject from circular ref-checking stack
} else { array_pop($state->subjectStack);
// include other values automatically }
$this->_addFrameOutput($list, '@list', self::copy($o)); }
}
} /**
continue; * Creates an implicit frame when recursing through subject matches. If
} * 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
if(self::_isSubjectReference($o)) { * parent frame used.
// recurse into subject reference *
$subframe = (property_exists($frame, $prop) ? * @param assoc flags the current framing flags.
$frame->{$prop} : $this->_createImplicitFrame($flags)); *
$this->_matchFrame( * @return array the implicit frame.
$state, array($o->{'@id'}), $subframe, $output, $prop); */
} else { function _createImplicitFrame($flags)
// include other values automatically {
$this->_addFrameOutput($output, $prop, self::copy($o)); $frame = new stdClass();
} foreach ($flags as $key => $value) {
} $frame->{'@' . $key} = [$flags[$key]];
} }
return [$frame];
// handle defaults }
$props = array_keys((array)$frame);
sort($props); /**
foreach($props as $prop) { * Checks the current subject stack to see if embedding the given subject
// skip keywords * would cause a circular reference.
if(self::_isKeyword($prop)) { *
continue; * @param stdClass subject_to_embed the subject to embed.
} * @param assoc subject_stack the current stack of subjects.
*
// if omit default is off, then include default values for properties * @return bool true if a circular reference would be created, false if not.
// that appear in the next frame but are not in the matching subject */
$next = $frame->{$prop}[0]; function _createsCircularReference($subject_to_embed, $subject_stack)
$omit_default_on = $this->_getFrameFlag( {
$next, $options, 'omitDefault'); for ($i = count($subject_stack) - 1; $i >= 0; --$i) {
if(!$omit_default_on && !property_exists($output, $prop)) { if ($subject_stack[$i]->{'@id'} === $subject_to_embed->{'@id'}) {
$preserve = '@null'; return true;
if(property_exists($next, '@default')) { }
$preserve = self::copy($next->{'@default'}); }
} return false;
$preserve = self::arrayify($preserve); }
$output->{$prop} = array((object)array('@preserve' => $preserve));
} /**
} * Gets the frame flag value for the given flag name.
*
// add output to parent * @param stdClass $frame the frame.
$this->_addFrameOutput($parent, $property, $output); * @param stdClass $options the framing options.
* @param string $name the flag name.
// pop matching subject from circular ref-checking stack *
array_pop($state->subjectStack); * @return mixed $the flag value.
} */
} protected function _getFrameFlag($frame, $options, $name)
{
/** $flag = "@$name";
* Creates an implicit frame when recursing through subject matches. If $rval = (property_exists($frame, $flag) ?
* a frame doesn't have an explicit frame for a particular property, then $frame->{$flag}[0] : $options[$name]);
* a wildcard child frame will be created that uses the same flags that the if ($name === 'embed') {
* parent frame used. // default is "@last"
* // backwards-compatibility support for "embed" maps:
* @param assoc flags the current framing flags. // true => "@last"
* // false => "@never"
* @return array the implicit frame. if ($rval === true) {
*/ $rval = '@last';
function _createImplicitFrame($flags) { } else if ($rval === false) {
$frame = new stdClass(); $rval = '@never';
foreach($flags as $key => $value) { } else if ($rval !== '@always' && $rval !== '@never' &&
$frame->{'@' . $key} = array($flags[$key]); $rval !== '@link') {
} $rval = '@last';
return array($frame); }
} }
return $rval;
/** }
* Checks the current subject stack to see if embedding the given subject
* would cause a circular reference. /**
* * Validates a JSON-LD frame, throwing an exception if the frame is invalid.
* @param stdClass subject_to_embed the subject to embed. *
* @param assoc subject_stack the current stack of subjects. * @param array $frame the frame to validate.
* */
* @return bool true if a circular reference would be created, false if not. protected function _validateFrame($frame)
*/ {
function _createsCircularReference($subject_to_embed, $subject_stack) { if (!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) {
for($i = count($subject_stack) - 1; $i >= 0; --$i) { throw new JsonLdException(
if($subject_stack[$i]->{'@id'} === $subject_to_embed->{'@id'}) { 'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.', 'jsonld.SyntaxError', null, ['frame' => $frame]);
return true; }
} }
}
return false; /**
} * Returns a map of all of the subjects that match a parsed frame.
*
/** * @param stdClass $state the current framing state.
* Gets the frame flag value for the given flag name. * @param array $subjects the set of subjects to filter.
* * @param stdClass $frame the parsed frame.
* @param stdClass $frame the frame. * @param assoc $flags the frame flags.
* @param stdClass $options the framing options. *
* @param string $name the flag name. * @return stdClass all of the matched subjects.
* */
* @return mixed $the flag value. protected function _filterSubjects($state, $subjects, $frame, $flags)
*/ {
protected function _getFrameFlag($frame, $options, $name) { $rval = new stdClass();
$flag = "@$name"; sort($subjects);
$rval = (property_exists($frame, $flag) ? foreach ($subjects as $id) {
$frame->{$flag}[0] : $options[$name]); $subject = $state->subjects->{$id};
if($name === 'embed') { if ($this->_filterSubject($subject, $frame, $flags)) {
// default is "@last" $rval->{$id} = $subject;
// backwards-compatibility support for "embed" maps: }
// true => "@last" }
// false => "@never" return $rval;
if($rval === true) { }
$rval = '@last';
} else if($rval === false) { /**
$rval = '@never'; * Returns true if the given subject matches the given frame.
} else if($rval !== '@always' && $rval !== '@never' && *
$rval !== '@link') { * @param stdClass $subject the subject to check.
$rval = '@last'; * @param stdClass $frame the frame to check.
} * @param assoc $flags the frame flags.
} *
return $rval; * @return bool true if the subject matches, false if not.
} */
protected function _filterSubject($subject, $frame, $flags)
/** {
* Validates a JSON-LD frame, throwing an exception if the frame is invalid. // check @type (object value means 'any' type, fall through to ducktyping)
* if (property_exists($frame, '@type') &&
* @param array $frame the frame to validate. !(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) {
*/ $types = $frame->{'@type'};
protected function _validateFrame($frame) { foreach ($types as $type) {
if(!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) { // any matching @type is a match
throw new JsonLdException( if (self::hasValue($subject, '@type', $type)) {
'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.', return true;
'jsonld.SyntaxError', null, array('frame' => $frame)); }
} }
} return false;
}
/**
* Returns a map of all of the subjects that match a parsed frame. // check ducktype
* $wildcard = true;
* @param stdClass $state the current framing state. $matches_some = false;
* @param array $subjects the set of subjects to filter. foreach ($frame as $k => $v) {
* @param stdClass $frame the parsed frame. if (self::_isKeyword($k)) {
* @param assoc $flags the frame flags. // skip non-@id and non-@type
* if ($k !== '@id' && $k !== '@type') {
* @return stdClass all of the matched subjects. continue;
*/ }
protected function _filterSubjects($state, $subjects, $frame, $flags) { $wildcard = false;
$rval = new stdClass();
sort($subjects); // check @id for a specific @id value
foreach($subjects as $id) { if ($k === '@id' && is_string($v)) {
$subject = $state->subjects->{$id}; if (!property_exists($subject, $k) || $subject->{$k} !== $v) {
if($this->_filterSubject($subject, $frame, $flags)) { return false;
$rval->{$id} = $subject; }
} $matches_some = true;
} continue;
return $rval; }
} }
/** $wildcard = false;
* Returns true if the given subject matches the given frame.
* if (property_exists($subject, $k)) {
* @param stdClass $subject the subject to check. // $v === [] means do not match if property is present
* @param stdClass $frame the frame to check. if (is_array($v) && count($v) === 0) {
* @param assoc $flags the frame flags. return false;
* }
* @return bool true if the subject matches, false if not. $matches_some = true;
*/ continue;
protected function _filterSubject($subject, $frame, $flags) { }
// check @type (object value means 'any' type, fall through to ducktyping)
if(property_exists($frame, '@type') && // all properties must match to be a duck unless a @default is specified
!(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) { $has_default = (is_array($v) && count($v) === 1 && is_object($v[0]) &&
$types = $frame->{'@type'}; property_exists($v[0], '@default'));
foreach($types as $type) { if ($flags['requireAll'] && !$has_default) {
// any matching @type is a match return false;
if(self::hasValue($subject, '@type', $type)) { }
return true; }
}
} // return true if wildcard or subject matches some properties
return false; return $wildcard || $matches_some;
} }
// check ducktype /**
$wildcard = true; * Removes an existing embed.
$matches_some = false; *
foreach($frame as $k => $v) { * @param stdClass $state the current framing state.
if(self::_isKeyword($k)) { * @param string $id the @id of the embed to remove.
// skip non-@id and non-@type */
if($k !== '@id' && $k !== '@type') { protected function _removeEmbed($state, $id)
continue; {
} // get existing embed
$wildcard = false; $embeds = $state->uniqueEmbeds;
$embed = $embeds->{$id};
// check @id for a specific @id value $property = $embed['property'];
if($k === '@id' && is_string($v)) {
if(!property_exists($subject, $k) || $subject->{$k} !== $v) { // create reference to replace embed
return false; $subject = (object) ['@id' => $id];
}
$matches_some = true; // remove existing embed
continue; if (is_array($embed->parent)) {
} // replace subject with reference
} foreach ($embed->parent as $i => $parent) {
if (self::compareValues($parent, $subject)) {
$wildcard = false; $embed->parent[$i] = $subject;
break;
if(property_exists($subject, $k)) { }
// $v === [] means do not match if property is present }
if(is_array($v) && count($v) === 0) { } else {
return false; // replace subject with reference
} $use_array = is_array($embed->parent->{$property});
$matches_some = true; self::removeValue($embed->parent, $property, $subject, ['propertyIsArray' => $use_array]);
continue; self::addValue($embed->parent, $property, $subject, ['propertyIsArray' => $use_array]);
} }
// all properties must match to be a duck unless a @default is specified // recursively remove dependent dangling embeds
$has_default = (is_array($v) && count($v) === 1 && is_object($v[0]) && $removeDependents = function($id) {
property_exists($v[0], '@default')); // get embed keys as a separate array to enable deleting keys in map
if($flags['requireAll'] && !$has_default) { $ids = array_keys((array) $embeds);
return false; foreach ($ids as $next) {
} if (property_exists($embeds, $next) &&
} is_object($embeds->{$next}->parent) &&
$embeds->{$next}->parent->{'@id'} === $id) {
// return true if wildcard or subject matches some properties unset($embeds->{$next});
return $wildcard || $matches_some; $removeDependents($next);
} }
}
/** };
* Removes an existing embed. $removeDependents($id);
* }
* @param stdClass $state the current framing state.
* @param string $id the @id of the embed to remove. /**
*/ * Adds framing output to the given parent.
protected function _removeEmbed($state, $id) { *
// get existing embed * @param mixed $parent the parent to add to.
$embeds = $state->uniqueEmbeds; * @param string $property the parent property.
$embed = $embeds->{$id}; * @param mixed $output the output to add.
$property = $embed['property']; */
protected function _addFrameOutput($parent, $property, $output)
// create reference to replace embed {
$subject = (object)array('@id' => $id); if (is_object($parent) && !($parent instanceof ArrayObject)) {
self::addValue(
// remove existing embed $parent, $property, $output, ['propertyIsArray' => true]);
if(is_array($embed->parent)) { } else {
// replace subject with reference $parent[] = $output;
foreach($embed->parent as $i => $parent) { }
if(self::compareValues($parent, $subject)) { }
$embed->parent[$i] = $subject;
break; /**
} * Removes the @preserve keywords as the last step of the framing algorithm.
} *
} else { * @param stdClass $ctx the active context used to compact the input.
// replace subject with reference * @param mixed $input the framed, compacted output.
$use_array = is_array($embed->parent->{$property}); * @param assoc $options the compaction options used.
self::removeValue($embed->parent, $property, $subject, *
array('propertyIsArray' => $use_array)); * @return mixed the resulting output.
self::addValue($embed->parent, $property, $subject, */
array('propertyIsArray' => $use_array)); protected function _removePreserve($ctx, $input, $options)
} {
// recurse through arrays
// recursively remove dependent dangling embeds if (is_array($input)) {
$removeDependents = function($id) { $output = [];
// get embed keys as a separate array to enable deleting keys in map foreach ($input as $e) {
$ids = array_keys((array)$embeds); $result = $this->_removePreserve($ctx, $e, $options);
foreach($ids as $next) { // drop nulls from arrays
if(property_exists($embeds, $next) && if ($result !== null) {
is_object($embeds->{$next}->parent) && $output[] = $result;
$embeds->{$next}->parent->{'@id'} === $id) { }
unset($embeds->{$next}); }
$removeDependents($next); $input = $output;
} } else if (is_object($input)) {
} // remove @preserve
}; if (property_exists($input, '@preserve')) {
$removeDependents($id); if ($input->{'@preserve'} === '@null') {
} return null;
}
/** return $input->{'@preserve'};
* Adds framing output to the given parent. }
*
* @param mixed $parent the parent to add to. // skip @values
* @param string $property the parent property. if (self::_isValue($input)) {
* @param mixed $output the output to add. return $input;
*/ }
protected function _addFrameOutput($parent, $property, $output) {
if(is_object($parent) && !($parent instanceof ArrayObject)) { // recurse through @lists
self::addValue( if (self::_isList($input)) {
$parent, $property, $output, array('propertyIsArray' => true)); $input->{'@list'} = $this->_removePreserve(
} else { $ctx, $input->{'@list'}, $options);
$parent[] = $output; return $input;
} }
}
// handle in-memory linked nodes
/** $id_alias = $this->_compactIri($ctx, '@id');
* Removes the @preserve keywords as the last step of the framing algorithm. if (property_exists($input, $id_alias)) {
* $id = $input->{$id_alias};
* @param stdClass $ctx the active context used to compact the input. if (isset($options['link'][$id])) {
* @param mixed $input the framed, compacted output. $idx = array_search($input, $options['link'][$id]);
* @param assoc $options the compaction options used. if ($idx === false) {
* // prevent circular visitation
* @return mixed the resulting output. $options['link'][$id][] = $input;
*/ } else {
protected function _removePreserve($ctx, $input, $options) { // already visited
// recurse through arrays return $options['link'][$id][$idx];
if(is_array($input)) { }
$output = array(); } else {
foreach($input as $e) { // prevent circular visitation
$result = $this->_removePreserve($ctx, $e, $options); $options['link'][$id] = [$input];
// drop nulls from arrays }
if($result !== null) { }
$output[] = $result;
} // recurse through properties
} foreach ($input as $prop => $v) {
$input = $output; $result = $this->_removePreserve($ctx, $v, $options);
} else if(is_object($input)) { $container = self::getContextValue($ctx, $prop, '@container');
// remove @preserve if ($options['compactArrays'] &&
if(property_exists($input, '@preserve')) { is_array($result) && count($result) === 1 &&
if($input->{'@preserve'} === '@null') { $container !== '@set' && $container !== '@list') {
return null; $result = $result[0];
} }
return $input->{'@preserve'}; $input->{$prop} = $result;
} }
}
// skip @values return $input;
if(self::_isValue($input)) { }
return $input;
} /**
* Compares two RDF triples for equality.
// recurse through @lists *
if(self::_isList($input)) { * @param stdClass $t1 the first triple.
$input->{'@list'} = $this->_removePreserve( * @param stdClass $t2 the second triple.
$ctx, $input->{'@list'}, $options); *
return $input; * @return true if the triples are the same, false if not.
} */
protected static function _compareRDFTriples($t1, $t2)
// handle in-memory linked nodes {
$id_alias = $this->_compactIri($ctx, '@id'); foreach (['subject', 'predicate', 'object'] as $attr) {
if(property_exists($input, $id_alias)) { if ($t1->{$attr}->type !== $t2->{$attr}->type ||
$id = $input->{$id_alias}; $t1->{$attr}->value !== $t2->{$attr}->value) {
if(isset($options['link'][$id])) { return false;
$idx = array_search($input, $options['link'][$id]); }
if($idx === false) { }
// prevent circular visitation if (property_exists($t1->object, 'language') !== property_exists($t1->object, 'language')) {
$options['link'][$id][] = $input; return false;
} else { }
// already visited if (property_exists($t1->object, 'language') &&
return $options['link'][$id][$idx]; $t1->object->language !== $t2->object->language) {
} return false;
} else { }
// prevent circular visitation if (property_exists($t1->object, 'datatype') &&
$options['link'][$id] = array($input); $t1->object->datatype !== $t2->object->datatype) {
} return false;
} }
return true;
// recurse through properties }
foreach($input as $prop => $v) {
$result = $this->_removePreserve($ctx, $v, $options); /**
$container = self::getContextValue($ctx, $prop, '@container'); * Hashes all of the quads about a blank node.
if($options['compactArrays'] && *
is_array($result) && count($result) === 1 && * @param string $id the ID of the bnode to hash quads for.
$container !== '@set' && $container !== '@list') { * @param stdClass $bnodes the mapping of bnodes to quads.
$result = $result[0]; * @param UniqueNamer $namer the canonical bnode namer.
} *
$input->{$prop} = $result; * @return string the new hash.
} */
} protected function _hashQuads($id, $bnodes, $namer)
return $input; {
} // return cached hash
if (property_exists($bnodes->{$id}, 'hash')) {
/** return $bnodes->{$id}->hash;
* Compares two RDF triples for equality. }
*
* @param stdClass $t1 the first triple. // serialize all of bnode's quads
* @param stdClass $t2 the second triple. $quads = $bnodes->{$id}->quads;
* $nquads = [];
* @return true if the triples are the same, false if not. foreach ($quads as $quad) {
*/ $nquads[] = $this->toNQuad($quad, property_exists($quad, 'name') ?
protected static function _compareRDFTriples($t1, $t2) { $quad->name->value : null, $id);
foreach(array('subject', 'predicate', 'object') as $attr) { }
if($t1->{$attr}->type !== $t2->{$attr}->type ||
$t1->{$attr}->value !== $t2->{$attr}->value) { // sort serialized quads
return false; sort($nquads);
}
} // cache and return hashed quads
if(property_exists($t1->object, 'language') !== $hash = $bnodes->{$id}->hash = sha1(implode($nquads));
property_exists($t1->object, 'language')) { return $hash;
return false; }
}
if(property_exists($t1->object, 'language') && /**
$t1->object->language !== $t2->object->language) { * Produces a hash for the paths of adjacent bnodes for a bnode,
return false; * incorporating all information about its subgraph of bnodes. This
} * method will recursively pick adjacent bnode permutations that produce the
if(property_exists($t1->object, 'datatype') && * lexicographically-least 'path' serializations.
$t1->object->datatype !== $t2->object->datatype) { *
return false; * @param string $id the ID of the bnode to hash paths for.
} * @param stdClass $bnodes the map of bnode quads.
return true; * @param UniqueNamer $namer the canonical bnode namer.
} * @param UniqueNamer $path_namer the namer used to assign names to adjacent
* bnodes.
/** *
* Hashes all of the quads about a blank node. * @return stdClass the hash and path namer used.
* */
* @param string $id the ID of the bnode to hash quads for. protected function _hashPaths($id, $bnodes, $namer, $path_namer)
* @param stdClass $bnodes the mapping of bnodes to quads. {
* @param UniqueNamer $namer the canonical bnode namer. // create SHA-1 digest
* $md = hash_init('sha1');
* @return string the new hash.
*/ // group adjacent bnodes by hash, keep properties and references separate
protected function _hashQuads($id, $bnodes, $namer) { $groups = new stdClass();
// return cached hash $quads = $bnodes->{$id}->quads;
if(property_exists($bnodes->{$id}, 'hash')) { foreach ($quads as $quad) {
return $bnodes->{$id}->hash; // get adjacent bnode
} $bnode = $this->_getAdjacentBlankNodeName($quad->subject, $id);
if ($bnode !== null) {
// serialize all of bnode's quads // normal property
$quads = $bnodes->{$id}->quads; $direction = 'p';
$nquads = array(); } else {
foreach($quads as $quad) { $bnode = $this->_getAdjacentBlankNodeName($quad->object, $id);
$nquads[] = $this->toNQuad($quad, property_exists($quad, 'name') ? if ($bnode !== null) {
$quad->name->value : null, $id); // reverse property
} $direction = 'r';
}
// sort serialized quads }
sort($nquads); if ($bnode !== null) {
// get bnode name (try canonical, path, then hash)
// cache and return hashed quads if ($namer->isNamed($bnode)) {
$hash = $bnodes->{$id}->hash = sha1(implode($nquads)); $name = $namer->getName($bnode);
return $hash; } else if ($path_namer->isNamed($bnode)) {
} $name = $path_namer->getName($bnode);
} else {
/** $name = $this->_hashQuads($bnode, $bnodes, $namer);
* Produces a hash for the paths of adjacent bnodes for a bnode, }
* incorporating all information about its subgraph of bnodes. This
* method will recursively pick adjacent bnode permutations that produce the // hash direction, property, and bnode name/hash
* lexicographically-least 'path' serializations. $group_md = hash_init('sha1');
* hash_update($group_md, $direction);
* @param string $id the ID of the bnode to hash paths for. hash_update($group_md, $quad->predicate->value);
* @param stdClass $bnodes the map of bnode quads. hash_update($group_md, $name);
* @param UniqueNamer $namer the canonical bnode namer. $group_hash = hash_final($group_md);
* @param UniqueNamer $path_namer the namer used to assign names to adjacent
* bnodes. // add bnode to hash group
* if (property_exists($groups, $group_hash)) {
* @return stdClass the hash and path namer used. $groups->{$group_hash}[] = $bnode;
*/ } else {
protected function _hashPaths($id, $bnodes, $namer, $path_namer) { $groups->{$group_hash} = [$bnode];
// create SHA-1 digest }
$md = hash_init('sha1'); }
}
// group adjacent bnodes by hash, keep properties and references separate
$groups = new stdClass(); // iterate over groups in sorted hash order
$quads = $bnodes->{$id}->quads; $group_hashes = array_keys((array) $groups);
foreach($quads as $quad) { sort($group_hashes);
// get adjacent bnode foreach ($group_hashes as $group_hash) {
$bnode = $this->_getAdjacentBlankNodeName($quad->subject, $id); // digest group hash
if($bnode !== null) { hash_update($md, $group_hash);
// normal property
$direction = 'p'; // choose a path and namer from the permutations
} else { $chosen_path = null;
$bnode = $this->_getAdjacentBlankNodeName($quad->object, $id); $chosen_namer = null;
if($bnode !== null) { $permutator = new Permutator($groups->{$group_hash});
// reverse property while ($permutator->hasNext()) {
$direction = 'r'; $permutation = $permutator->next();
} $path_namer_copy = clone $path_namer;
}
if($bnode !== null) { // build adjacent path
// get bnode name (try canonical, path, then hash) $path = '';
if($namer->isNamed($bnode)) { $skipped = false;
$name = $namer->getName($bnode); $recurse = [];
} else if($path_namer->isNamed($bnode)) { foreach ($permutation as $bnode) {
$name = $path_namer->getName($bnode); // use canonical name if available
} else { if ($namer->isNamed($bnode)) {
$name = $this->_hashQuads($bnode, $bnodes, $namer); $path .= $namer->getName($bnode);
} } else {
// recurse if bnode isn't named in the path yet
// hash direction, property, and bnode name/hash if (!$path_namer_copy->isNamed($bnode)) {
$group_md = hash_init('sha1'); $recurse[] = $bnode;
hash_update($group_md, $direction); }
hash_update($group_md, $quad->predicate->value); $path .= $path_namer_copy->getName($bnode);
hash_update($group_md, $name); }
$group_hash = hash_final($group_md);
// skip permutation if path is already >= chosen path
// add bnode to hash group if ($chosen_path !== null && strlen($path) >= strlen($chosen_path) &&
if(property_exists($groups, $group_hash)) { $path > $chosen_path) {
$groups->{$group_hash}[] = $bnode; $skipped = true;
} else { break;
$groups->{$group_hash} = array($bnode); }
} }
}
} // recurse
if (!$skipped) {
// iterate over groups in sorted hash order foreach ($recurse as $bnode) {
$group_hashes = array_keys((array)$groups); $result = $this->_hashPaths(
sort($group_hashes); $bnode, $bnodes, $namer, $path_namer_copy);
foreach($group_hashes as $group_hash) { $path .= $path_namer_copy->getName($bnode);
// digest group hash $path .= "<{$result->hash}>";
hash_update($md, $group_hash); $path_namer_copy = $result->pathNamer;
// choose a path and namer from the permutations // skip permutation if path is already >= chosen path
$chosen_path = null; if ($chosen_path !== null &&
$chosen_namer = null; strlen($path) >= strlen($chosen_path) && $path > $chosen_path) {
$permutator = new Permutator($groups->{$group_hash}); $skipped = true;
while($permutator->hasNext()) { break;
$permutation = $permutator->next(); }
$path_namer_copy = clone $path_namer; }
}
// build adjacent path
$path = ''; if (!$skipped && ($chosen_path === null || $path < $chosen_path)) {
$skipped = false; $chosen_path = $path;
$recurse = array(); $chosen_namer = $path_namer_copy;
foreach($permutation as $bnode) { }
// use canonical name if available }
if($namer->isNamed($bnode)) {
$path .= $namer->getName($bnode); // digest chosen path and update namer
} else { hash_update($md, $chosen_path);
// recurse if bnode isn't named in the path yet $path_namer = $chosen_namer;
if(!$path_namer_copy->isNamed($bnode)) { }
$recurse[] = $bnode;
} // return SHA-1 hash and path namer
$path .= $path_namer_copy->getName($bnode); return (object) [
} 'hash' => hash_final($md), 'pathNamer' => $path_namer];
}
// skip permutation if path is already >= chosen path
if($chosen_path !== null && strlen($path) >= strlen($chosen_path) && /**
$path > $chosen_path) { * A helper function that gets the blank node name from an RDF quad
$skipped = true; * node (subject or object). If the node is not a blank node or its
break; * value does not match the given blank node ID, it will be returned.
} *
} * @param stdClass $node the RDF quad node.
* @param string $id the ID of the blank node to look next to.
// recurse *
if(!$skipped) { * @return mixed the adjacent blank node name or null if none was found.
foreach($recurse as $bnode) { */
$result = $this->_hashPaths( protected function _getAdjacentBlankNodeName($node, $id)
$bnode, $bnodes, $namer, $path_namer_copy); {
$path .= $path_namer_copy->getName($bnode); if ($node->type === 'blank node' && $node->value !== $id) {
$path .= "<{$result->hash}>"; return $node->value;
$path_namer_copy = $result->pathNamer; }
return null;
// skip permutation if path is already >= chosen path }
if($chosen_path !== null &&
strlen($path) >= strlen($chosen_path) && $path > $chosen_path) { /**
$skipped = true; * Compares two strings first based on length and then lexicographically.
break; *
} * @param string $a the first string.
} * @param string $b the second string.
} *
* @return integer -1 if a < b, 1 if a > b, 0 if a == b.
if(!$skipped && ($chosen_path === null || $path < $chosen_path)) { */
$chosen_path = $path; protected function _compareShortestLeast($a, $b)
$chosen_namer = $path_namer_copy; {
} $len_a = strlen($a);
} $len_b = strlen($b);
if ($len_a < $len_b) {
// digest chosen path and update namer return -1;
hash_update($md, $chosen_path); }
$path_namer = $chosen_namer; if ($len_b < $len_a) {
} return 1;
}
// return SHA-1 hash and path namer if ($a === $b) {
return (object)array( return 0;
'hash' => hash_final($md), 'pathNamer' => $path_namer); }
} return ($a < $b) ? -1 : 1;
}
/**
* A helper function that gets the blank node name from an RDF quad /**
* node (subject or object). If the node is not a blank node or its * Picks the preferred compaction term from the given inverse context entry.
* value does not match the given blank node ID, it will be returned. *
* * @param active_ctx the active context.
* @param stdClass $node the RDF quad node. * @param iri the IRI to pick the term for.
* @param string $id the ID of the blank node to look next to. * @param value the value to pick the term for.
* * @param containers the preferred containers.
* @return mixed the adjacent blank node name or null if none was found. * @param type_or_language either '@type' or '@language'.
*/ * @param type_or_language_value the preferred value for '@type' or
protected function _getAdjacentBlankNodeName($node, $id) { * '@language'.
if($node->type === 'blank node' && $node->value !== $id) { *
return $node->value; * @return mixed the preferred term.
} */
return null; protected function _selectTerm(
} $active_ctx, $iri, $value, $containers, $type_or_language, $type_or_language_value)
{
/** if ($type_or_language_value === null) {
* Compares two strings first based on length and then lexicographically. $type_or_language_value = '@null';
* }
* @param string $a the first string.
* @param string $b the second string. // options for the value of @type or @language
* $prefs = [];
* @return integer -1 if a < b, 1 if a > b, 0 if a == b.
*/ // determine prefs for @id based on whether or not value compacts to a term
protected function _compareShortestLeast($a, $b) { if (($type_or_language_value === '@id' ||
$len_a = strlen($a); $type_or_language_value === '@reverse') &&
$len_b = strlen($b); self::_isSubjectReference($value)) {
if($len_a < $len_b) { // prefer @reverse first
return -1; if ($type_or_language_value === '@reverse') {
} $prefs[] = '@reverse';
if($len_b < $len_a) { }
return 1; // try to compact value to a term
} $term = $this->_compactIri(
if($a === $b) { $active_ctx, $value->{'@id'}, null, ['vocab' => true]);
return 0; if (property_exists($active_ctx->mappings, $term) &&
} $active_ctx->mappings->{$term} &&
return ($a < $b) ? -1 : 1; $active_ctx->mappings->{$term}->{'@id'} === $value->{'@id'}) {
} // prefer @vocab
array_push($prefs, '@vocab', '@id');
/** } else {
* Picks the preferred compaction term from the given inverse context entry. // prefer @id
* array_push($prefs, '@id', '@vocab');
* @param active_ctx the active context. }
* @param iri the IRI to pick the term for. } else {
* @param value the value to pick the term for. $prefs[] = $type_or_language_value;
* @param containers the preferred containers. }
* @param type_or_language either '@type' or '@language'. $prefs[] = '@none';
* @param type_or_language_value the preferred value for '@type' or
* '@language'. $container_map = $active_ctx->inverse->{$iri};
* foreach ($containers as $container) {
* @return mixed the preferred term. // if container not available in the map, continue
*/ if (!property_exists($container_map, $container)) {
protected function _selectTerm( continue;
$active_ctx, $iri, $value, $containers, }
$type_or_language, $type_or_language_value) {
if($type_or_language_value === null) { $type_or_language_value_map = $container_map->{$container}->{$type_or_language};
$type_or_language_value = '@null'; foreach ($prefs as $pref) {
} // if type/language option not available in the map, continue
if (!property_exists($type_or_language_value_map, $pref)) {
// options for the value of @type or @language continue;
$prefs = array(); }
// determine prefs for @id based on whether or not value compacts to a term // select term
if(($type_or_language_value === '@id' || return $type_or_language_value_map->{$pref};
$type_or_language_value === '@reverse') && }
self::_isSubjectReference($value)) { }
// prefer @reverse first return null;
if($type_or_language_value === '@reverse') { }
$prefs[] = '@reverse';
} /**
// try to compact value to a term * Compacts an IRI or keyword into a term or prefix if it can be. If the
$term = $this->_compactIri( * IRI has an associated value it may be passed.
$active_ctx, $value->{'@id'}, null, array('vocab' => true)); *
if(property_exists($active_ctx->mappings, $term) && * @param stdClass $active_ctx the active context to use.
$active_ctx->mappings->{$term} && * @param string $iri the IRI to compact.
$active_ctx->mappings->{$term}->{'@id'} === $value->{'@id'}) { * @param mixed $value the value to check or null.
// prefer @vocab * @param assoc $relative_to options for how to compact IRIs:
array_push($prefs, '@vocab', '@id'); * vocab: true to split after @vocab, false not to.
} else { * @param bool $reverse true if a reverse property is being compacted, false
// prefer @id * if not.
array_push($prefs, '@id', '@vocab'); *
} * @return string the compacted term, prefix, keyword alias, or original IRI.
} else { */
$prefs[] = $type_or_language_value; protected function _compactIri(
} $active_ctx, $iri, $value = null, $relative_to = [], $reverse = false)
$prefs[] = '@none'; {
// can't compact null
$container_map = $active_ctx->inverse->{$iri}; if ($iri === null) {
foreach($containers as $container) { return $iri;
// if container not available in the map, continue }
if(!property_exists($container_map, $container)) {
continue; $inverse_ctx = $this->_getInverseContext($active_ctx);
}
if (self::_isKeyword($iri)) {
$type_or_language_value_map = // a keyword can only be compacted to simple alias
$container_map->{$container}->{$type_or_language}; if (property_exists($inverse_ctx, $iri)) {
foreach($prefs as $pref) { return $inverse_ctx->$iri->{'@none'}->{'@type'}->{'@none'};
// if type/language option not available in the map, continue }
if(!property_exists($type_or_language_value_map, $pref)) { return $iri;
continue; }
}
if (!isset($relative_to['vocab'])) {
// select term $relative_to['vocab'] = false;
return $type_or_language_value_map->{$pref}; }
}
} // use inverse context to pick a term if iri is relative to vocab
return null; if ($relative_to['vocab'] && property_exists($inverse_ctx, $iri)) {
} $default_language = '@none';
if (property_exists($active_ctx, '@language')) {
/** $default_language = $active_ctx->{'@language'};
* 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.
* // prefer @index if available in value
* @param stdClass $active_ctx the active context to use. $containers = [];
* @param string $iri the IRI to compact. if (is_object($value) && property_exists($value, '@index')) {
* @param mixed $value the value to check or null. $containers[] = '@index';
* @param assoc $relative_to options for how to compact IRIs: }
* vocab: true to split after @vocab, false not to.
* @param bool $reverse true if a reverse property is being compacted, false // defaults for term selection based on type/language
* if not. $type_or_language = '@language';
* $type_or_language_value = '@null';
* @return string the compacted term, prefix, keyword alias, or original IRI.
*/ if ($reverse) {
protected function _compactIri( $type_or_language = '@type';
$active_ctx, $iri, $value=null, $relative_to=array(), $reverse=false) { $type_or_language_value = '@reverse';
// can't compact null $containers[] = '@set';
if($iri === null) { } else if (self::_isList($value)) {
return $iri; // choose the most specific term that works for all elements in @list
} // only select @list containers if @index is NOT in value
if (!property_exists($value, '@index')) {
$inverse_ctx = $this->_getInverseContext($active_ctx); $containers[] = '@list';
}
if(self::_isKeyword($iri)) { $list = $value->{'@list'};
// a keyword can only be compacted to simple alias $common_language = (count($list) === 0) ? $default_language : null;
if(property_exists($inverse_ctx, $iri)) { $common_type = null;
return $inverse_ctx->$iri->{'@none'}->{'@type'}->{'@none'}; foreach ($list as $item) {
} $item_language = '@none';
return $iri; $item_type = '@none';
} if (self::_isValue($item)) {
if (property_exists($item, '@language')) {
if(!isset($relative_to['vocab'])) { $item_language = $item->{'@language'};
$relative_to['vocab'] = false; } else if (property_exists($item, '@type')) {
} $item_type = $item->{'@type'};
} else {
// use inverse context to pick a term if iri is relative to vocab // plain literal
if($relative_to['vocab'] && property_exists($inverse_ctx, $iri)) { $item_language = '@null';
$default_language = '@none'; }
if(property_exists($active_ctx, '@language')) { } else {
$default_language = $active_ctx->{'@language'}; $item_type = '@id';
} }
if ($common_language === null) {
// prefer @index if available in value $common_language = $item_language;
$containers = array(); } else if ($item_language !== $common_language &&
if(is_object($value) && property_exists($value, '@index')) { self::_isValue($item)) {
$containers[] = '@index'; $common_language = '@none';
} }
if ($common_type === null) {
// defaults for term selection based on type/language $common_type = $item_type;
$type_or_language = '@language'; } else if ($item_type !== $common_type) {
$type_or_language_value = '@null'; $common_type = '@none';
}
if($reverse) { // there are different languages and types in the list, so choose
$type_or_language = '@type'; // the most generic term, no need to keep iterating the list
$type_or_language_value = '@reverse'; if ($common_language === '@none' && $common_type === '@none') {
$containers[] = '@set'; break;
} else if(self::_isList($value)) { }
// choose the most specific term that works for all elements in @list }
// only select @list containers if @index is NOT in value if ($common_language === null) {
if(!property_exists($value, '@index')) { $common_language = '@none';
$containers[] = '@list'; }
} if ($common_type === null) {
$list = $value->{'@list'}; $common_type = '@none';
$common_language = (count($list) === 0) ? $default_language : null; }
$common_type = null; if ($common_type !== '@none') {
foreach($list as $item) { $type_or_language = '@type';
$item_language = '@none'; $type_or_language_value = $common_type;
$item_type = '@none'; } else {
if(self::_isValue($item)) { $type_or_language_value = $common_language;
if(property_exists($item, '@language')) { }
$item_language = $item->{'@language'}; } else {
} else if(property_exists($item, '@type')) { if (self::_isValue($value)) {
$item_type = $item->{'@type'}; if (property_exists($value, '@language') &&
} else { !property_exists($value, '@index')) {
// plain literal $containers[] = '@language';
$item_language = '@null'; $type_or_language_value = $value->{'@language'};
} } else if (property_exists($value, '@type')) {
} else { $type_or_language = '@type';
$item_type = '@id'; $type_or_language_value = $value->{'@type'};
} }
if($common_language === null) { } else {
$common_language = $item_language; $type_or_language = '@type';
} else if($item_language !== $common_language && $type_or_language_value = '@id';
self::_isValue($item)) { }
$common_language = '@none'; $containers[] = '@set';
} }
if($common_type === null) {
$common_type = $item_type; // do term selection
} else if($item_type !== $common_type) { $containers[] = '@none';
$common_type = '@none'; $term = $this->_selectTerm(
} $active_ctx, $iri, $value, $containers, $type_or_language, $type_or_language_value);
// there are different languages and types in the list, so choose if ($term !== null) {
// the most generic term, no need to keep iterating the list return $term;
if($common_language === '@none' && $common_type === '@none') { }
break; }
}
} // no term match, use @vocab if available
if($common_language === null) { if ($relative_to['vocab']) {
$common_language = '@none'; if (property_exists($active_ctx, '@vocab')) {
} // determine if vocab is a prefix of the iri
if($common_type === null) { $vocab = $active_ctx->{'@vocab'};
$common_type = '@none'; if (strpos($iri, $vocab) === 0 && $iri !== $vocab) {
} // use suffix as relative iri if it is not a term in the active
if($common_type !== '@none') { // context
$type_or_language = '@type'; $suffix = substr($iri, strlen($vocab));
$type_or_language_value = $common_type; if (!property_exists($active_ctx->mappings, $suffix)) {
} else { return $suffix;
$type_or_language_value = $common_language; }
} }
} else { }
if(self::_isValue($value)) { }
if(property_exists($value, '@language') &&
!property_exists($value, '@index')) { // no term or @vocab match, check for possible CURIEs
$containers[] = '@language'; $choice = null;
$type_or_language_value = $value->{'@language'}; $idx = 0;
} else if(property_exists($value, '@type')) { $partial_matches = [];
$type_or_language = '@type'; $iri_map = $active_ctx->fast_curie_map;
$type_or_language_value = $value->{'@type'}; // check for partial matches of against `iri`, which means look until
} // iri.length - 1, not full length
} else { $max_partial_length = strlen($iri) - 1;
$type_or_language = '@type'; for (; $idx < $max_partial_length && isset($iri_map[$iri[$idx]]); ++$idx) {
$type_or_language_value = '@id'; $iri_map = $iri_map[$iri[$idx]];
} if (isset($iri_map[''])) {
$containers[] = '@set'; $entry = $iri_map[''][0];
} $entry->iri_length = $idx + 1;
$partial_matches[] = $entry;
// do term selection }
$containers[] = '@none'; }
$term = $this->_selectTerm( // check partial matches in reverse order to prefer longest ones first
$active_ctx, $iri, $value, $partial_matches = array_reverse($partial_matches);
$containers, $type_or_language, $type_or_language_value); foreach ($partial_matches as $entry) {
if($term !== null) { $terms = $entry->terms;
return $term; foreach ($terms as $term) {
} // a CURIE is usable if:
} // 1. it has no mapping, OR
// 2. value is null, which means we're not compacting an @value, AND
// no term match, use @vocab if available // the mapping matches the IRI
if($relative_to['vocab']) { $curie = $term . ':' . substr($iri, $entry->iri_length);
if(property_exists($active_ctx, '@vocab')) { $is_usable_curie = (!property_exists($active_ctx->mappings, $curie) ||
// determine if vocab is a prefix of the iri ($value === null &&
$vocab = $active_ctx->{'@vocab'}; $active_ctx->mappings->{$curie}->{'@id'} === $iri));
if(strpos($iri, $vocab) === 0 && $iri !== $vocab) {
// use suffix as relative iri if it is not a term in the active // select curie if it is shorter or the same length but
// context // lexicographically less than the current choice
$suffix = substr($iri, strlen($vocab)); if ($is_usable_curie && ($choice === null ||
if(!property_exists($active_ctx->mappings, $suffix)) { self::_compareShortestLeast($curie, $choice) < 0)) {
return $suffix; $choice = $curie;
} }
} }
} }
}
// return chosen curie
// no term or @vocab match, check for possible CURIEs if ($choice !== null) {
$choice = null; return $choice;
$idx = 0; }
$partial_matches = array();
$iri_map = $active_ctx->fast_curie_map; // compact IRI relative to base
// check for partial matches of against `iri`, which means look until if (!$relative_to['vocab']) {
// iri.length - 1, not full length return jsonld_remove_base($active_ctx->{'@base'}, $iri);
$max_partial_length = strlen($iri) - 1; }
for(; $idx < $max_partial_length && isset($iri_map[$iri[$idx]]); ++$idx) {
$iri_map = $iri_map[$iri[$idx]]; // return IRI as is
if(isset($iri_map[''])) { return $iri;
$entry = $iri_map[''][0]; }
$entry->iri_length = $idx + 1;
$partial_matches[] = $entry; /**
} * Performs value compaction on an object with '@value' or '@id' as the only
} * property.
// check partial matches in reverse order to prefer longest ones first *
$partial_matches = array_reverse($partial_matches); * @param stdClass $active_ctx the active context.
foreach($partial_matches as $entry) { * @param string $active_property the active property that points to the
$terms = $entry->terms; * value.
foreach($terms as $term) { * @param mixed $value the value to compact.
// a CURIE is usable if: *
// 1. it has no mapping, OR * @return mixed the compaction result.
// 2. value is null, which means we're not compacting an @value, AND */
// the mapping matches the IRI protected function _compactValue($active_ctx, $active_property, $value)
$curie = $term . ':' . substr($iri, $entry->iri_length); {
$is_usable_curie = (!property_exists($active_ctx->mappings, $curie) || // value is a @value
($value === null && if (self::_isValue($value)) {
$active_ctx->mappings->{$curie}->{'@id'} === $iri)); // get context rules
$type = self::getContextValue($active_ctx, $active_property, '@type');
// select curie if it is shorter or the same length but $language = self::getContextValue(
// lexicographically less than the current choice $active_ctx, $active_property, '@language');
if($is_usable_curie && ($choice === null || $container = self::getContextValue(
self::_compareShortestLeast($curie, $choice) < 0)) { $active_ctx, $active_property, '@container');
$choice = $curie;
} // whether or not the value has an @index that must be preserved
} $preserve_index = (property_exists($value, '@index') &&
} $container !== '@index');
// return chosen curie // if there's no @index to preserve
if($choice !== null) { if (!$preserve_index) {
return $choice; // matching @type or @language specified in context, compact value
} if (self::_hasKeyValue($value, '@type', $type) ||
self::_hasKeyValue($value, '@language', $language)) {
// compact IRI relative to base return $value->{'@value'};
if(!$relative_to['vocab']) { }
return jsonld_remove_base($active_ctx->{'@base'}, $iri); }
}
// return just the value of @value if all are true:
// return IRI as is // 1. @value is the only key or @index isn't being preserved
return $iri; // 2. there is no default language or @value is not a string or
} // the key has a mapping with a null @language
$key_count = count(array_keys((array) $value));
/** $is_value_only_key = ($key_count === 1 ||
* Performs value compaction on an object with '@value' or '@id' as the only ($key_count === 2 && property_exists($value, '@index') &&
* property. !$preserve_index));
* $has_default_language = property_exists($active_ctx, '@language');
* @param stdClass $active_ctx the active context. $is_value_string = is_string($value->{'@value'});
* @param string $active_property the active property that points to the $has_null_mapping = (
* value. property_exists($active_ctx->mappings, $active_property) &&
* @param mixed $value the value to compact. $active_ctx->mappings->{$active_property} !== null &&
* self::_hasKeyValue(
* @return mixed the compaction result. $active_ctx->mappings->{$active_property}, '@language', null));
*/ if ($is_value_only_key &&
protected function _compactValue($active_ctx, $active_property, $value) { (!$has_default_language || !$is_value_string || $has_null_mapping)) {
// value is a @value return $value->{'@value'};
if(self::_isValue($value)) { }
// get context rules
$type = self::getContextValue($active_ctx, $active_property, '@type'); $rval = new stdClass();
$language = self::getContextValue(
$active_ctx, $active_property, '@language'); // preserve @index
$container = self::getContextValue( if ($preserve_index) {
$active_ctx, $active_property, '@container'); $rval->{$this->_compactIri($active_ctx, '@index')} = $value->{'@index'};
}
// whether or not the value has an @index that must be preserved
$preserve_index = (property_exists($value, '@index') && // compact @type IRI
$container !== '@index'); if (property_exists($value, '@type')) {
$rval->{$this->_compactIri($active_ctx, '@type')} = $this->_compactIri(
// if there's no @index to preserve $active_ctx, $value->{'@type'}, null, ['vocab' => true]);
if(!$preserve_index) { } else if (property_exists($value, '@language')) {
// matching @type or @language specified in context, compact value // alias @language
if(self::_hasKeyValue($value, '@type', $type) || $rval->{$this->_compactIri($active_ctx, '@language')} = $value->{'@language'};
self::_hasKeyValue($value, '@language', $language)) { }
return $value->{'@value'};
} // alias @value
} $rval->{$this->_compactIri($active_ctx, '@value')} = $value->{'@value'};
// return just the value of @value if all are true: return $rval;
// 1. @value is the only key or @index isn't being preserved }
// 2. there is no default language or @value is not a string or
// the key has a mapping with a null @language // value is a subject reference
$key_count = count(array_keys((array)$value)); $expanded_property = $this->_expandIri(
$is_value_only_key = ($key_count === 1 || $active_ctx, $active_property, ['vocab' => true]);
($key_count === 2 && property_exists($value, '@index') && $type = self::getContextValue($active_ctx, $active_property, '@type');
!$preserve_index)); $compacted = $this->_compactIri(
$has_default_language = property_exists($active_ctx, '@language'); $active_ctx, $value->{'@id'}, null, ['vocab' => ($type === '@vocab')]);
$is_value_string = is_string($value->{'@value'});
$has_null_mapping = ( // compact to scalar
property_exists($active_ctx->mappings, $active_property) && if ($type === '@id' || $type === '@vocab' ||
$active_ctx->mappings->{$active_property} !== null && $expanded_property === '@graph') {
self::_hasKeyValue( return $compacted;
$active_ctx->mappings->{$active_property}, '@language', null)); }
if($is_value_only_key &&
(!$has_default_language || !$is_value_string || $has_null_mapping)) { $rval = (object) [
return $value->{'@value'}; $this->_compactIri($active_ctx, '@id') => $compacted];
} return $rval;
}
$rval = new stdClass();
/**
// preserve @index * Creates a term definition during context processing.
if($preserve_index) { *
$rval->{$this->_compactIri($active_ctx, '@index')} = $value->{'@index'}; * @param stdClass $active_ctx the current active context.
} * @param stdClass $local_ctx the local context being processed.
* @param string $term the key in the local context to define the mapping for.
// compact @type IRI * @param stdClass $defined a map of defining/defined keys to detect cycles
if(property_exists($value, '@type')) { * and prevent double definitions.
$rval->{$this->_compactIri($active_ctx, '@type')} = $this->_compactIri( */
$active_ctx, $value->{'@type'}, null, array('vocab' => true)); protected function _createTermDefinition(
} else if(property_exists($value, '@language')) { $active_ctx, $local_ctx, $term, $defined)
// alias @language {
$rval->{$this->_compactIri($active_ctx, '@language')} = if (property_exists($defined, $term)) {
$value->{'@language'}; // term already defined
} if ($defined->{$term}) {
return;
// alias @value }
$rval->{$this->_compactIri($active_ctx, '@value')} = $value->{'@value'}; // cycle detected
throw new JsonLdException(
return $rval; 'Cyclical context definition detected.', 'jsonld.CyclicalContext', 'cyclic IRI mapping', ['context' => $local_ctx, 'term' => $term]);
} }
// value is a subject reference // now defining term
$expanded_property = $this->_expandIri( $defined->{$term} = false;
$active_ctx, $active_property, array('vocab' => true));
$type = self::getContextValue($active_ctx, $active_property, '@type'); if (self::_isKeyword($term)) {
$compacted = $this->_compactIri( throw new JsonLdException(
$active_ctx, $value->{'@id'}, null, 'Invalid JSON-LD syntax; keywords cannot be overridden.', 'jsonld.SyntaxError', 'keyword redefinition', ['context' => $local_ctx, 'term' => $term]);
array('vocab' => ($type === '@vocab'))); }
// compact to scalar // remove old mapping
if($type === '@id' || $type === '@vocab' || if (property_exists($active_ctx->mappings, $term)) {
$expanded_property === '@graph') { unset($active_ctx->mappings->{$term});
return $compacted; }
}
// get context term value
$rval = (object)array( $value = $local_ctx->{$term};
$this->_compactIri($active_ctx, '@id') => $compacted);
return $rval; // clear context entry
} if ($value === null || (is_object($value) &&
self::_hasKeyValue($value, '@id', null))) {
/** $active_ctx->mappings->{$term} = null;
* Creates a term definition during context processing. $defined->{$term} = true;
* return;
* @param stdClass $active_ctx the current active context. }
* @param stdClass $local_ctx the local context being processed.
* @param string $term the key in the local context to define the mapping for. // convert short-hand value to object w/@id
* @param stdClass $defined a map of defining/defined keys to detect cycles if (is_string($value)) {
* and prevent double definitions. $value = (object) ['@id' => $value];
*/ }
protected function _createTermDefinition(
$active_ctx, $local_ctx, $term, $defined) { if (!is_object($value)) {
if(property_exists($defined, $term)) { throw new JsonLdException(
// term already defined 'Invalid JSON-LD syntax; @context property values must be ' .
if($defined->{$term}) { 'strings or objects.', 'jsonld.SyntaxError', 'invalid term definition', ['context' => $local_ctx]);
return; }
}
// cycle detected // create new mapping
throw new JsonLdException( $mapping = $active_ctx->mappings->{$term} = new stdClass();
'Cyclical context definition detected.', $mapping->reverse = false;
'jsonld.CyclicalContext', 'cyclic IRI mapping',
array('context' => $local_ctx, 'term' => $term)); if (property_exists($value, '@reverse')) {
} if (property_exists($value, '@id')) {
throw new JsonLdException(
// now defining term 'Invalid JSON-LD syntax; a @reverse term definition must not ' +
$defined->{$term} = false; 'contain @id.', 'jsonld.SyntaxError', 'invalid reverse property', ['context' => $local_ctx]);
}
if(self::_isKeyword($term)) { $reverse = $value->{'@reverse'};
throw new JsonLdException( if (!is_string($reverse)) {
'Invalid JSON-LD syntax; keywords cannot be overridden.', throw new JsonLdException(
'jsonld.SyntaxError', 'keyword redefinition', 'Invalid JSON-LD syntax; a @context @reverse value must be a string.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
array('context' => $local_ctx, 'term' => $term)); }
}
// expand and add @id mapping
// remove old mapping $id = $this->_expandIri(
if(property_exists($active_ctx->mappings, $term)) { $active_ctx, $reverse, ['vocab' => true, 'base' => false], $local_ctx, $defined);
unset($active_ctx->mappings->{$term}); if (!self::_isAbsoluteIri($id)) {
} throw new JsonLdException(
'Invalid JSON-LD syntax; @context @reverse value must be ' .
// get context term value 'an absolute IRI or a blank node identifier.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
$value = $local_ctx->{$term}; }
$mapping->{'@id'} = $id;
// clear context entry $mapping->reverse = true;
if($value === null || (is_object($value) && } else if (property_exists($value, '@id')) {
self::_hasKeyValue($value, '@id', null))) { $id = $value->{'@id'};
$active_ctx->mappings->{$term} = null; if (!is_string($id)) {
$defined->{$term} = true; throw new JsonLdException(
return; 'Invalid JSON-LD syntax; @context @id value must be a string.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
} }
if ($id !== $term) {
// convert short-hand value to object w/@id // add @id to mapping
if(is_string($value)) { $id = $this->_expandIri(
$value = (object)array('@id' => $value); $active_ctx, $id, ['vocab' => true, 'base' => false], $local_ctx, $defined);
} if (!self::_isAbsoluteIri($id) && !self::_isKeyword($id)) {
throw new JsonLdException(
if(!is_object($value)) { 'Invalid JSON-LD syntax; @context @id value must be an ' .
throw new JsonLdException( 'absolute IRI, a blank node identifier, or a keyword.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]);
'Invalid JSON-LD syntax; @context property values must be ' . }
'strings or objects.', 'jsonld.SyntaxError', 'invalid term definition', $mapping->{'@id'} = $id;
array('context' => $local_ctx)); }
} }
// create new mapping // always compute whether term has a colon as an optimization for
$mapping = $active_ctx->mappings->{$term} = new stdClass(); // _compactIri
$mapping->reverse = false; $colon = strpos($term, ':');
$mapping->_term_has_colon = ($colon !== false);
if(property_exists($value, '@reverse')) {
if(property_exists($value, '@id')) { if (!property_exists($mapping, '@id')) {
throw new JsonLdException( // see if the term has a prefix
'Invalid JSON-LD syntax; a @reverse term definition must not ' + if ($mapping->_term_has_colon) {
'contain @id.', 'jsonld.SyntaxError', 'invalid reverse property', $prefix = substr($term, 0, $colon);
array('context' => $local_ctx)); if (property_exists($local_ctx, $prefix)) {
} // define parent prefix
$reverse = $value->{'@reverse'}; $this->_createTermDefinition(
if(!is_string($reverse)) { $active_ctx, $local_ctx, $prefix, $defined);
throw new JsonLdException( }
'Invalid JSON-LD syntax; a @context @reverse value must be a string.',
'jsonld.SyntaxError', 'invalid IRI mapping', if (property_exists($active_ctx->mappings, $prefix) &&
array('context' => $local_ctx)); $active_ctx->mappings->{$prefix}) {
} // set @id based on prefix parent
$suffix = substr($term, $colon + 1);
// expand and add @id mapping $mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} .
$id = $this->_expandIri( $suffix;
$active_ctx, $reverse, array('vocab' => true, 'base' => false), } else {
$local_ctx, $defined); // term is an absolute IRI
if(!self::_isAbsoluteIri($id)) { $mapping->{'@id'} = $term;
throw new JsonLdException( }
'Invalid JSON-LD syntax; @context @reverse value must be ' . } else {
'an absolute IRI or a blank node identifier.', // non-IRIs *must* define @ids if @vocab is not available
'jsonld.SyntaxError', 'invalid IRI mapping', if (!property_exists($active_ctx, '@vocab')) {
array('context' => $local_ctx)); throw new JsonLdException(
} 'Invalid JSON-LD syntax; @context terms must define an @id.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx, 'term' => $term]);
$mapping->{'@id'} = $id; }
$mapping->reverse = true; // prepend vocab to term
} else if(property_exists($value, '@id')) { $mapping->{'@id'} = $active_ctx->{'@vocab'} . $term;
$id = $value->{'@id'}; }
if(!is_string($id)) { }
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @id value must be a string.', // optimization to store length of @id once for _compactIri
'jsonld.SyntaxError', 'invalid IRI mapping', $mapping->_id_length = strlen($mapping->{'@id'});
array('context' => $local_ctx));
} // IRI mapping now defined
if($id !== $term) { $defined->{$term} = true;
// add @id to mapping
$id = $this->_expandIri( if (property_exists($value, '@type')) {
$active_ctx, $id, array('vocab' => true, 'base' => false), $type = $value->{'@type'};
$local_ctx, $defined); if (!is_string($type)) {
if(!self::_isAbsoluteIri($id) && !self::_isKeyword($id)) { throw new JsonLdException(
throw new JsonLdException( 'Invalid JSON-LD syntax; @context @type values must be strings.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]);
'Invalid JSON-LD syntax; @context @id value must be an ' . }
'absolute IRI, a blank node identifier, or a keyword.',
'jsonld.SyntaxError', 'invalid IRI mapping', if ($type !== '@id' && $type !== '@vocab') {
array('context' => $local_ctx)); // expand @type to full IRI
} $type = $this->_expandIri(
$mapping->{'@id'} = $id; $active_ctx, $type, ['vocab' => true], $local_ctx, $defined);
} if (!self::_isAbsoluteIri($type)) {
} throw new JsonLdException(
'Invalid JSON-LD syntax; an @context @type value must ' .
// always compute whether term has a colon as an optimization for 'be an absolute IRI.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]);
// _compactIri }
$colon = strpos($term, ':'); if (strpos($type, '_:') === 0) {
$mapping->_term_has_colon = ($colon !== false); throw new JsonLdException(
'Invalid JSON-LD syntax; an @context @type values must ' .
if(!property_exists($mapping, '@id')) { 'be an IRI, not a blank node identifier.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]);
// see if the term has a prefix }
if($mapping->_term_has_colon) { }
$prefix = substr($term, 0, $colon);
if(property_exists($local_ctx, $prefix)) { // add @type to mapping
// define parent prefix $mapping->{'@type'} = $type;
$this->_createTermDefinition( }
$active_ctx, $local_ctx, $prefix, $defined);
} if (property_exists($value, '@container')) {
$container = $value->{'@container'};
if(property_exists($active_ctx->mappings, $prefix) && if ($container !== '@list' && $container !== '@set' &&
$active_ctx->mappings->{$prefix}) { $container !== '@index' && $container !== '@language') {
// set @id based on prefix parent throw new JsonLdException(
$suffix = substr($term, $colon + 1); 'Invalid JSON-LD syntax; @context @container value must be ' .
$mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} . 'one of the following: @list, @set, @index, or @language.', 'jsonld.SyntaxError', 'invalid container mapping', ['context' => $local_ctx]);
$suffix; }
} else { if ($mapping->reverse && $container !== '@index' &&
// term is an absolute IRI $container !== '@set' && $container !== null) {
$mapping->{'@id'} = $term; throw new JsonLdException(
} 'Invalid JSON-LD syntax; @context @container value for a @reverse ' +
} else { 'type definition must be @index or @set.', 'jsonld.SyntaxError', 'invalid reverse property', ['context' => $local_ctx]);
// non-IRIs *must* define @ids if @vocab is not available }
if(!property_exists($active_ctx, '@vocab')) {
throw new JsonLdException( // add @container to mapping
'Invalid JSON-LD syntax; @context terms must define an @id.', $mapping->{'@container'} = $container;
'jsonld.SyntaxError', 'invalid IRI mapping', }
array('context' => $local_ctx, 'term' => $term));
} if (property_exists($value, '@language') &&
// prepend vocab to term !property_exists($value, '@type')) {
$mapping->{'@id'} = $active_ctx->{'@vocab'} . $term; $language = $value->{'@language'};
} if ($language !== null && !is_string($language)) {
} throw new JsonLdException(
'Invalid JSON-LD syntax; @context @language value must be ' .
// optimization to store length of @id once for _compactIri 'a string or null.', 'jsonld.SyntaxError', 'invalid language mapping', ['context' => $local_ctx]);
$mapping->_id_length = strlen($mapping->{'@id'}); }
// IRI mapping now defined // add @language to mapping
$defined->{$term} = true; if ($language !== null) {
$language = strtolower($language);
if(property_exists($value, '@type')) { }
$type = $value->{'@type'}; $mapping->{'@language'} = $language;
if(!is_string($type)) { }
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @type values must be strings.', // disallow aliasing @context and @preserve
'jsonld.SyntaxError', 'invalid type mapping', $id = $mapping->{'@id'};
array('context' => $local_ctx)); if ($id === '@context' || $id === '@preserve') {
} throw new JsonLdException(
'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.', 'jsonld.SyntaxError', 'invalid keyword alias', ['context' => $local_ctx]);
if($type !== '@id' && $type !== '@vocab') { }
// expand @type to full IRI }
$type = $this->_expandIri(
$active_ctx, $type, array('vocab' => true), $local_ctx, $defined); /**
if(!self::_isAbsoluteIri($type)) { * Expands a string to a full IRI. The string may be a term, a prefix, a
throw new JsonLdException( * relative IRI, or an absolute IRI. The associated absolute IRI will be
'Invalid JSON-LD syntax; an @context @type value must ' . * returned.
'be an absolute IRI.', 'jsonld.SyntaxError', *
'invalid type mapping', array('context' => $local_ctx)); * @param stdClass $active_ctx the current active context.
} * @param string $value the string to expand.
if(strpos($type, '_:') === 0) { * @param assoc $relative_to options for how to resolve relative IRIs:
throw new JsonLdException( * base: true to resolve against the base IRI, false not to.
'Invalid JSON-LD syntax; an @context @type values must ' . * vocab: true to concatenate after @vocab, false not to.
'be an IRI, not a blank node identifier.', * @param stdClass $local_ctx the local context being processed (only given
'jsonld.SyntaxError', 'invalid type mapping', * if called during document processing).
array('context' => $local_ctx)); * @param defined a map for tracking cycles in context definitions (only given
} * if called during document processing).
} *
* @return mixed the expanded value.
// add @type to mapping */
$mapping->{'@type'} = $type; function _expandIri(
} $active_ctx, $value, $relative_to = [], $local_ctx = null, $defined = null)
{
if(property_exists($value, '@container')) { // already expanded
$container = $value->{'@container'}; if ($value === null || self::_isKeyword($value)) {
if($container !== '@list' && $container !== '@set' && return $value;
$container !== '@index' && $container !== '@language') { }
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @container value must be ' . // define term dependency if not defined
'one of the following: @list, @set, @index, or @language.', if ($local_ctx !== null && property_exists($local_ctx, $value) &&
'jsonld.SyntaxError', 'invalid container mapping', !self::_hasKeyValue($defined, $value, true)) {
array('context' => $local_ctx)); $this->_createTermDefinition($active_ctx, $local_ctx, $value, $defined);
} }
if($mapping->reverse && $container !== '@index' &&
$container !== '@set' && $container !== null) { if (isset($relative_to['vocab']) && $relative_to['vocab']) {
throw new JsonLdException( if (property_exists($active_ctx->mappings, $value)) {
'Invalid JSON-LD syntax; @context @container value for a @reverse ' + $mapping = $active_ctx->mappings->{$value};
'type definition must be @index or @set.',
'jsonld.SyntaxError', 'invalid reverse property', // value is explicitly ignored with a null mapping
array('context' => $local_ctx)); if ($mapping === null) {
} return null;
}
// add @container to mapping
$mapping->{'@container'} = $container; // value is a term
} return $mapping->{'@id'};
}
if(property_exists($value, '@language') && }
!property_exists($value, '@type')) {
$language = $value->{'@language'}; // split value into prefix:suffix
if($language !== null && !is_string($language)) { $colon = strpos($value, ':');
throw new JsonLdException( if ($colon !== false) {
'Invalid JSON-LD syntax; @context @language value must be ' . $prefix = substr($value, 0, $colon);
'a string or null.', 'jsonld.SyntaxError', $suffix = substr($value, $colon + 1);
'invalid language mapping', array('context' => $local_ctx));
} // do not expand blank nodes (prefix of '_') or already-absolute
// IRIs (suffix of '//')
// add @language to mapping if ($prefix === '_' || strpos($suffix, '//') === 0) {
if($language !== null) { return $value;
$language = strtolower($language); }
}
$mapping->{'@language'} = $language; // prefix dependency not defined, define it
} if ($local_ctx !== null && property_exists($local_ctx, $prefix)) {
$this->_createTermDefinition(
// disallow aliasing @context and @preserve $active_ctx, $local_ctx, $prefix, $defined);
$id = $mapping->{'@id'}; }
if($id === '@context' || $id === '@preserve') {
throw new JsonLdException( // use mapping if prefix is defined
'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.', if (property_exists($active_ctx->mappings, $prefix)) {
'jsonld.SyntaxError', 'invalid keyword alias', $mapping = $active_ctx->mappings->{$prefix};
array('context' => $local_ctx)); if ($mapping) {
} return $mapping->{'@id'} . $suffix;
} }
}
/**
* Expands a string to a full IRI. The string may be a term, a prefix, a // already absolute IRI
* relative IRI, or an absolute IRI. The associated absolute IRI will be return $value;
* returned. }
*
* @param stdClass $active_ctx the current active context. // prepend vocab
* @param string $value the string to expand. if (isset($relative_to['vocab']) && $relative_to['vocab'] &&
* @param assoc $relative_to options for how to resolve relative IRIs: property_exists($active_ctx, '@vocab')) {
* base: true to resolve against the base IRI, false not to. return $active_ctx->{'@vocab'} . $value;
* vocab: true to concatenate after @vocab, false not to. }
* @param stdClass $local_ctx the local context being processed (only given
* if called during document processing). // prepend base
* @param defined a map for tracking cycles in context definitions (only given $rval = $value;
* if called during document processing). if (isset($relative_to['base']) && $relative_to['base']) {
* $rval = jsonld_prepend_base($active_ctx->{'@base'}, $rval);
* @return mixed the expanded value. }
*/
function _expandIri( return $rval;
$active_ctx, $value, $relative_to=array(), $local_ctx=null, $defined=null) { }
// already expanded
if($value === null || self::_isKeyword($value)) { /**
return $value; * Finds all @context URLs in the given JSON-LD input.
} *
* @param mixed $input the JSON-LD input.
// define term dependency if not defined * @param stdClass $urls a map of URLs (url => false/@contexts).
if($local_ctx !== null && property_exists($local_ctx, $value) && * @param bool $replace true to replace the URLs in the given input with
!self::_hasKeyValue($defined, $value, true)) { * the @contexts from the urls map, false not to.
$this->_createTermDefinition($active_ctx, $local_ctx, $value, $defined); * @param string $base the base URL to resolve relative URLs with.
} */
protected function _findContextUrls($input, $urls, $replace, $base)
if(isset($relative_to['vocab']) && $relative_to['vocab']) { {
if(property_exists($active_ctx->mappings, $value)) { if (is_array($input)) {
$mapping = $active_ctx->mappings->{$value}; foreach ($input as $e) {
$this->_findContextUrls($e, $urls, $replace, $base);
// value is explicitly ignored with a null mapping }
if($mapping === null) { } else if (is_object($input)) {
return null; foreach ($input as $k => &$v) {
} if ($k !== '@context') {
$this->_findContextUrls($v, $urls, $replace, $base);
// value is a term continue;
return $mapping->{'@id'}; }
}
} // array @context
if (is_array($v)) {
// split value into prefix:suffix $length = count($v);
$colon = strpos($value, ':'); for ($i = 0; $i < $length; ++$i) {
if($colon !== false) { if (is_string($v[$i])) {
$prefix = substr($value, 0, $colon); $url = jsonld_prepend_base($base, $v[$i]);
$suffix = substr($value, $colon + 1); // replace w/@context if requested
if ($replace) {
// do not expand blank nodes (prefix of '_') or already-absolute $ctx = $urls->{$url};
// IRIs (suffix of '//') if (is_array($ctx)) {
if($prefix === '_' || strpos($suffix, '//') === 0) { // add flattened context
return $value; array_splice($v, $i, 1, $ctx);
} $i += count($ctx) - 1;
$length = count($v);
// prefix dependency not defined, define it } else {
if($local_ctx !== null && property_exists($local_ctx, $prefix)) { $v[$i] = $ctx;
$this->_createTermDefinition( }
$active_ctx, $local_ctx, $prefix, $defined); } else if (!property_exists($urls, $url)) {
} // @context URL found
$urls->{$url} = false;
// use mapping if prefix is defined }
if(property_exists($active_ctx->mappings, $prefix)) { }
$mapping = $active_ctx->mappings->{$prefix}; }
if($mapping) { } else if (is_string($v)) {
return $mapping->{'@id'} . $suffix; // string @context
} $v = jsonld_prepend_base($base, $v);
} // replace w/@context if requested
if ($replace) {
// already absolute IRI $input->{$k} = $urls->{$v};
return $value; } else if (!property_exists($urls, $v)) {
} // @context URL found
$urls->{$v} = false;
// prepend vocab }
if(isset($relative_to['vocab']) && $relative_to['vocab'] && }
property_exists($active_ctx, '@vocab')) { }
return $active_ctx->{'@vocab'} . $value; }
} }
// prepend base /**
$rval = $value; * Retrieves external @context URLs using the given document loader. Each
if(isset($relative_to['base']) && $relative_to['base']) { * instance of @context in the input that refers to a URL will be replaced
$rval = jsonld_prepend_base($active_ctx->{'@base'}, $rval); * with the JSON @context found at that URL.
} *
* @param mixed $input the JSON-LD input with possible contexts.
return $rval; * @param stdClass $cycles an object for tracking context cycles.
} * @param callable $load_document(url) the document loader.
* @param base $base the base URL to resolve relative URLs against.
/** *
* Finds all @context URLs in the given JSON-LD input. * @return mixed the result.
* */
* @param mixed $input the JSON-LD input. protected function _retrieveContextUrls(
* @param stdClass $urls a map of URLs (url => false/@contexts). &$input, $cycles, $load_document, $base = '')
* @param bool $replace true to replace the URLs in the given input with {
* the @contexts from the urls map, false not to. if (count(get_object_vars($cycles)) > self::MAX_CONTEXT_URLS) {
* @param string $base the base URL to resolve relative URLs with. throw new JsonLdException(
*/ 'Maximum number of @context URLs exceeded.', 'jsonld.ContextUrlError', 'loading remote context failed', ['max' => self::MAX_CONTEXT_URLS]);
protected function _findContextUrls($input, $urls, $replace, $base) { }
if(is_array($input)) {
foreach($input as $e) { // for tracking the URLs to retrieve
$this->_findContextUrls($e, $urls, $replace, $base); $urls = new stdClass();
}
} else if(is_object($input)) { // regex for validating URLs
foreach($input as $k => &$v) { $regex = '/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/';
if($k !== '@context') {
$this->_findContextUrls($v, $urls, $replace, $base); // find all URLs in the given input
continue; $this->_findContextUrls($input, $urls, false, $base);
}
// queue all unretrieved URLs
// array @context $queue = [];
if(is_array($v)) { foreach ($urls as $url => $ctx) {
$length = count($v); if ($ctx === false) {
for($i = 0; $i < $length; ++$i) { // validate URL
if(is_string($v[$i])) { if (!preg_match($regex, $url)) {
$url = jsonld_prepend_base($base, $v[$i]); throw new JsonLdException(
// replace w/@context if requested 'Malformed or unsupported URL.', 'jsonld.InvalidUrl', 'loading remote context failed', ['url' => $url]);
if($replace) { }
$ctx = $urls->{$url}; $queue[] = $url;
if(is_array($ctx)) { }
// add flattened context }
array_splice($v, $i, 1, $ctx);
$i += count($ctx) - 1; // retrieve URLs in queue
$length = count($v); foreach ($queue as $url) {
} else { // check for context URL cycle
$v[$i] = $ctx; if (property_exists($cycles, $url)) {
} throw new JsonLdException(
} else if(!property_exists($urls, $url)) { 'Cyclical @context URLs detected.', 'jsonld.ContextUrlError', 'recursive context inclusion', ['url' => $url]);
// @context URL found }
$urls->{$url} = false; $_cycles = self::copy($cycles);
} $_cycles->{$url} = true;
}
} // retrieve URL
} else if(is_string($v)) { $remote_doc = call_user_func($load_document, $url);
// string @context $ctx = $remote_doc->document;
$v = jsonld_prepend_base($base, $v);
// replace w/@context if requested // parse string context as JSON
if($replace) { if (is_string($ctx)) {
$input->{$k} = $urls->{$v}; try {
} else if(!property_exists($urls, $v)) { $ctx = self::_parse_json($ctx);
// @context URL found } catch (Exception $e) {
$urls->{$v} = false; throw new JsonLdException(
} 'Could not parse JSON from URL.', 'jsonld.ParseError', 'loading remote context failed', ['url' => $url], $e);
} }
} }
}
} // ensure ctx is an object
if (!is_object($ctx)) {
/** throw new JsonLdException(
* Retrieves external @context URLs using the given document loader. Each 'Derefencing a URL did not result in a valid JSON-LD object.', 'jsonld.InvalidUrl', 'invalid remote context', ['url' => $url]);
* instance of @context in the input that refers to a URL will be replaced }
* with the JSON @context found at that URL.
* // use empty context if no @context key is present
* @param mixed $input the JSON-LD input with possible contexts. if (!property_exists($ctx, '@context')) {
* @param stdClass $cycles an object for tracking context cycles. $ctx = (object) ['@context' => new stdClass()];
* @param callable $load_document(url) the document loader. } else {
* @param base $base the base URL to resolve relative URLs against. $ctx = (object) ['@context' => $ctx->{'@context'}];
* }
* @return mixed the result.
*/ // append context URL to context if given
protected function _retrieveContextUrls( if ($remote_doc->contextUrl !== null) {
&$input, $cycles, $load_document, $base='') { $ctx->{'@context'} = self::arrayify($ctx->{'@context'});
if(count(get_object_vars($cycles)) > self::MAX_CONTEXT_URLS) { $ctx->{'@context'}[] = $remote_doc->contextUrl;
throw new JsonLdException( }
'Maximum number of @context URLs exceeded.',
'jsonld.ContextUrlError', 'loading remote context failed', // recurse
array('max' => self::MAX_CONTEXT_URLS)); $this->_retrieveContextUrls($ctx, $_cycles, $load_document, $url);
} $urls->{$url} = $ctx->{'@context'};
}
// for tracking the URLs to retrieve
$urls = new stdClass(); // replace all URLS in the input
$this->_findContextUrls($input, $urls, true, $base);
// regex for validating URLs }
$regex = '/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/';
/**
// find all URLs in the given input * Gets the initial context.
$this->_findContextUrls($input, $urls, false, $base); *
* @param assoc $options the options to use.
// queue all unretrieved URLs * base the document base IRI.
$queue = array(); *
foreach($urls as $url => $ctx) { * @return stdClass the initial context.
if($ctx === false) { */
// validate URL protected function _getInitialContext($options)
if(!preg_match($regex, $url)) { {
throw new JsonLdException( return (object) [
'Malformed or unsupported URL.', 'jsonld.InvalidUrl', '@base' => jsonld_parse_url($options['base']),
'loading remote context failed', array('url' => $url)); 'mappings' => new stdClass(),
} 'inverse' => null];
$queue[] = $url; }
}
} /**
* Generates an inverse context for use in the compaction algorithm, if
// retrieve URLs in queue * not already generated for the given active context.
foreach($queue as $url) { *
// check for context URL cycle * @param stdClass $active_ctx the active context to use.
if(property_exists($cycles, $url)) { *
throw new JsonLdException( * @return stdClass the inverse context.
'Cyclical @context URLs detected.', */
'jsonld.ContextUrlError', 'recursive context inclusion', protected function _getInverseContext($active_ctx)
array('url' => $url)); {
} // inverse context already generated
$_cycles = self::copy($cycles); if ($active_ctx->inverse) {
$_cycles->{$url} = true; return $active_ctx->inverse;
}
// retrieve URL
$remote_doc = call_user_func($load_document, $url); $inverse = $active_ctx->inverse = new stdClass();
$ctx = $remote_doc->document;
// variables for building fast CURIE map
// parse string context as JSON $fast_curie_map = $active_ctx->fast_curie_map = new ArrayObject();
if(is_string($ctx)) { $iris_to_terms = [];
try {
$ctx = self::_parse_json($ctx); // handle default language
} catch(Exception $e) { $default_language = '@none';
throw new JsonLdException( if (property_exists($active_ctx, '@language')) {
'Could not parse JSON from URL.', $default_language = $active_ctx->{'@language'};
'jsonld.ParseError', 'loading remote context failed', }
array('url' => $url), $e);
} // create term selections for each mapping in the context, ordered by
} // shortest and then lexicographically least
$mappings = $active_ctx->mappings;
// ensure ctx is an object $terms = array_keys((array) $mappings);
if(!is_object($ctx)) { usort($terms, [$this, '_compareShortestLeast']);
throw new JsonLdException( foreach ($terms as $term) {
'Derefencing a URL did not result in a valid JSON-LD object.', $mapping = $mappings->{$term};
'jsonld.InvalidUrl', 'invalid remote context', array('url' => $url)); if ($mapping === null) {
} continue;
}
// use empty context if no @context key is present
if(!property_exists($ctx, '@context')) { // add term selection where it applies
$ctx = (object)array('@context' => new stdClass()); if (property_exists($mapping, '@container')) {
} else { $container = $mapping->{'@container'};
$ctx = (object)array('@context' => $ctx->{'@context'}); } else {
} $container = '@none';
}
// append context URL to context if given
if($remote_doc->contextUrl !== null) { // iterate over every IRI in the mapping
$ctx->{'@context'} = self::arrayify($ctx->{'@context'}); $iris = $mapping->{'@id'};
$ctx->{'@context'}[] = $remote_doc->contextUrl; $iris = self::arrayify($iris);
} foreach ($iris as $iri) {
$is_keyword = self::_isKeyword($iri);
// recurse
$this->_retrieveContextUrls($ctx, $_cycles, $load_document, $url); // initialize container map
$urls->{$url} = $ctx->{'@context'}; if (!property_exists($inverse, $iri)) {
} $inverse->{$iri} = new stdClass();
if (!$is_keyword && !$mapping->_term_has_colon) {
// replace all URLS in the input // init IRI to term map and fast CURIE map
$this->_findContextUrls($input, $urls, true, $base); $iris_to_terms[$iri] = new ArrayObject();
} $iris_to_terms[$iri][] = $term;
$fast_curie_entry = (object) [
/** 'iri' => $iri, 'terms' => $iris_to_terms[$iri]];
* Gets the initial context. if (!array_key_exists($iri[0], (array) $fast_curie_map)) {
* $fast_curie_map[$iri[0]] = new ArrayObject();
* @param assoc $options the options to use. }
* base the document base IRI. $fast_curie_map[$iri[0]][] = $fast_curie_entry;
* }
* @return stdClass the initial context. } else if (!$is_keyword && !$mapping->_term_has_colon) {
*/ // add IRI to term match
protected function _getInitialContext($options) { $iris_to_terms[$iri][] = $term;
return (object)array( }
'@base' => jsonld_parse_url($options['base']), $container_map = $inverse->{$iri};
'mappings' => new stdClass(),
'inverse' => null); // add new entry
} if (!property_exists($container_map, $container)) {
$container_map->{$container} = (object) [
/** '@language' => new stdClass(),
* Generates an inverse context for use in the compaction algorithm, if '@type' => new stdClass()];
* not already generated for the given active context. }
* $entry = $container_map->{$container};
* @param stdClass $active_ctx the active context to use.
* if ($mapping->reverse) {
* @return stdClass the inverse context. // term is preferred for values using @reverse
*/ $this->_addPreferredTerm(
protected function _getInverseContext($active_ctx) { $mapping, $term, $entry->{'@type'}, '@reverse');
// inverse context already generated } else if (property_exists($mapping, '@type')) {
if($active_ctx->inverse) { // term is preferred for values using specific type
return $active_ctx->inverse; $this->_addPreferredTerm(
} $mapping, $term, $entry->{'@type'}, $mapping->{'@type'});
} else if (property_exists($mapping, '@language')) {
$inverse = $active_ctx->inverse = new stdClass(); // term is preferred for values using specific language
$language = $mapping->{'@language'};
// variables for building fast CURIE map if ($language === null) {
$fast_curie_map = $active_ctx->fast_curie_map = new ArrayObject(); $language = '@null';
$iris_to_terms = array(); }
$this->_addPreferredTerm(
// handle default language $mapping, $term, $entry->{'@language'}, $language);
$default_language = '@none'; } else {
if(property_exists($active_ctx, '@language')) { // term is preferred for values w/default language or no type and
$default_language = $active_ctx->{'@language'}; // no language
} // add an entry for the default language
$this->_addPreferredTerm(
// create term selections for each mapping in the context, ordered by $mapping, $term, $entry->{'@language'}, $default_language);
// shortest and then lexicographically least
$mappings = $active_ctx->mappings; // add entries for no type and no language
$terms = array_keys((array)$mappings); $this->_addPreferredTerm(
usort($terms, array($this, '_compareShortestLeast')); $mapping, $term, $entry->{'@type'}, '@none');
foreach($terms as $term) { $this->_addPreferredTerm(
$mapping = $mappings->{$term}; $mapping, $term, $entry->{'@language'}, '@none');
if($mapping === null) { }
continue; }
} }
// add term selection where it applies // build fast CURIE map
if(property_exists($mapping, '@container')) { foreach ($fast_curie_map as $key => $value) {
$container = $mapping->{'@container'}; $this->_buildIriMap($fast_curie_map, $key, 1);
} else { }
$container = '@none';
} return $inverse;
}
// iterate over every IRI in the mapping
$iris = $mapping->{'@id'}; /**
$iris = self::arrayify($iris); * Runs a recursive algorithm to build a lookup map for quickly finding
foreach($iris as $iri) { * potential CURIEs.
$is_keyword = self::_isKeyword($iri); *
* @param ArrayObject $iri_map the map to build.
// initialize container map * @param string $key the current key in the map to work on.
if(!property_exists($inverse, $iri)) { * @param int $idx the index into the IRI to compare.
$inverse->{$iri} = new stdClass(); */
if(!$is_keyword && !$mapping->_term_has_colon) { function _buildIriMap($iri_map, $key, $idx)
// init IRI to term map and fast CURIE map {
$iris_to_terms[$iri] = new ArrayObject(); $entries = $iri_map[$key];
$iris_to_terms[$iri][] = $term; $next = $iri_map[$key] = new ArrayObject();
$fast_curie_entry = (object)array(
'iri' => $iri, 'terms' => $iris_to_terms[$iri]); foreach ($entries as $entry) {
if(!array_key_exists($iri[0], (array)$fast_curie_map)) { $iri = $entry->iri;
$fast_curie_map[$iri[0]] = new ArrayObject(); if ($idx >= strlen($iri)) {
} $letter = '';
$fast_curie_map[$iri[0]][] = $fast_curie_entry; } else {
} $letter = $iri[$idx];
} else if(!$is_keyword && !$mapping->_term_has_colon) { }
// add IRI to term match if (!isset($next[$letter])) {
$iris_to_terms[$iri][] = $term; $next[$letter] = new ArrayObject();
} }
$container_map = $inverse->{$iri}; $next[$letter][] = $entry;
}
// add new entry
if(!property_exists($container_map, $container)) { foreach ($next as $key => $value) {
$container_map->{$container} = (object)array( if ($key === '') {
'@language' => new stdClass(), continue;
'@type' => new stdClass()); }
} $this->_buildIriMap($next, $key, $idx + 1);
$entry = $container_map->{$container}; }
}
if($mapping->reverse) {
// term is preferred for values using @reverse /**
$this->_addPreferredTerm( * Adds the term for the given entry if not already added.
$mapping, $term, $entry->{'@type'}, '@reverse'); *
} else if(property_exists($mapping, '@type')) { * @param stdClass $mapping the term mapping.
// term is preferred for values using specific type * @param string $term the term to add.
$this->_addPreferredTerm( * @param stdClass $entry the inverse context type_or_language entry to
$mapping, $term, $entry->{'@type'}, $mapping->{'@type'}); * add to.
} else if(property_exists($mapping, '@language')) { * @param string $type_or_language_value the key in the entry to add to.
// term is preferred for values using specific language */
$language = $mapping->{'@language'}; function _addPreferredTerm($mapping, $term, $entry, $type_or_language_value)
if($language === null) { {
$language = '@null'; if (!property_exists($entry, $type_or_language_value)) {
} $entry->{$type_or_language_value} = $term;
$this->_addPreferredTerm( }
$mapping, $term, $entry->{'@language'}, $language); }
} else {
// term is preferred for values w/default language or no type and /**
// no language * Clones an active context, creating a child active context.
// add an entry for the default language *
$this->_addPreferredTerm( * @return stdClass a clone (child) of the active context.
$mapping, $term, $entry->{'@language'}, $default_language); */
protected function _cloneActiveContext($active_ctx)
// add entries for no type and no language {
$this->_addPreferredTerm( $child = new stdClass();
$mapping, $term, $entry->{'@type'}, '@none'); $child->{'@base'} = $active_ctx->{'@base'};
$this->_addPreferredTerm( $child->mappings = self::copy($active_ctx->mappings);
$mapping, $term, $entry->{'@language'}, '@none'); $child->inverse = null;
} if (property_exists($active_ctx, '@language')) {
} $child->{'@language'} = $active_ctx->{'@language'};
} }
if (property_exists($active_ctx, '@vocab')) {
// build fast CURIE map $child->{'@vocab'} = $active_ctx->{'@vocab'};
foreach($fast_curie_map as $key => $value) { }
$this->_buildIriMap($fast_curie_map, $key, 1); return $child;
} }
return $inverse; /**
} * Returns whether or not the given value is a keyword.
*
/** * @param string $v the value to check.
* Runs a recursive algorithm to build a lookup map for quickly finding *
* potential CURIEs. * @return bool true if the value is a keyword, false if not.
* */
* @param ArrayObject $iri_map the map to build. protected static function _isKeyword($v)
* @param string $key the current key in the map to work on. {
* @param int $idx the index into the IRI to compare. if (!is_string($v)) {
*/ return false;
function _buildIriMap($iri_map, $key, $idx) { }
$entries = $iri_map[$key]; switch ($v) {
$next = $iri_map[$key] = new ArrayObject(); case '@base':
case '@context':
foreach($entries as $entry) { case '@container':
$iri = $entry->iri; case '@default':
if($idx >= strlen($iri)) { case '@embed':
$letter = ''; case '@explicit':
} else { case '@graph':
$letter = $iri[$idx]; case '@id':
} case '@index':
if(!isset($next[$letter])) { case '@language':
$next[$letter] = new ArrayObject(); case '@list':
} case '@omitDefault':
$next[$letter][] = $entry; case '@preserve':
} case '@requireAll':
case '@reverse':
foreach($next as $key => $value) { case '@set':
if($key === '') { case '@type':
continue; case '@value':
} case '@vocab':
$this->_buildIriMap($next, $key, $idx + 1); return true;
} }
} return false;
}
/**
* Adds the term for the given entry if not already added. /**
* * Returns true if the given value is an empty Object.
* @param stdClass $mapping the term mapping. *
* @param string $term the term to add. * @param mixed $v the value to check.
* @param stdClass $entry the inverse context type_or_language entry to *
* add to. * @return bool true if the value is an empty Object, false if not.
* @param string $type_or_language_value the key in the entry to add to. */
*/ protected static function _isEmptyObject($v)
function _addPreferredTerm($mapping, $term, $entry, $type_or_language_value) { {
if(!property_exists($entry, $type_or_language_value)) { return is_object($v) && count(get_object_vars($v)) === 0;
$entry->{$type_or_language_value} = $term; }
}
} /**
* Throws an exception if the given value is not a valid @type 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. protected static function _validateTypeValue($v)
*/ {
protected function _cloneActiveContext($active_ctx) { // must be a string or empty object
$child = new stdClass(); if (is_string($v) || self::_isEmptyObject($v)) {
$child->{'@base'} = $active_ctx->{'@base'}; return;
$child->mappings = self::copy($active_ctx->mappings); }
$child->inverse = null;
if(property_exists($active_ctx, '@language')) { // must be an array
$child->{'@language'} = $active_ctx->{'@language'}; $is_valid = false;
} if (is_array($v)) {
if(property_exists($active_ctx, '@vocab')) { // must contain only strings
$child->{'@vocab'} = $active_ctx->{'@vocab'}; $is_valid = true;
} foreach ($v as $e) {
return $child; if (!(is_string($e))) {
} $is_valid = false;
break;
/** }
* Returns whether or not the given value is a keyword. }
* }
* @param string $v the value to check.
* if (!$is_valid) {
* @return bool true if the value is a keyword, false if not. throw new JsonLdException(
*/ 'Invalid JSON-LD syntax; "@type" value must a string, an array ' .
protected static function _isKeyword($v) { 'of strings, or an empty object.', 'jsonld.SyntaxError', 'invalid type value', ['value' => $v]);
if(!is_string($v)) { }
return false; }
}
switch($v) { /**
case '@base': * Returns true if the given value is a subject with properties.
case '@context': *
case '@container': * @param mixed $v the value to check.
case '@default': *
case '@embed': * @return bool true if the value is a subject with properties, false if not.
case '@explicit': */
case '@graph': protected static function _isSubject($v)
case '@id': {
case '@index': // Note: A value is a subject if all of these hold true:
case '@language': // 1. It is an Object.
case '@list': // 2. It is not a @value, @set, or @list.
case '@omitDefault': // 3. It has more than 1 key OR any existing key is not @id.
case '@preserve': $rval = false;
case '@requireAll': if (is_object($v) &&
case '@reverse': !property_exists($v, '@value') &&
case '@set': !property_exists($v, '@set') &&
case '@type': !property_exists($v, '@list')) {
case '@value': $count = count(get_object_vars($v));
case '@vocab': $rval = ($count > 1 || !property_exists($v, '@id'));
return true; }
} return $rval;
return false; }
}
/**
/** * Returns true if the given value is a subject reference.
* Returns true if the given value is an empty Object. *
* * @param mixed $v the value to check.
* @param mixed $v the value to check. *
* * @return bool true if the value is a subject reference, false if not.
* @return bool true if the value is an empty Object, false if not. */
*/ protected static function _isSubjectReference($v)
protected static function _isEmptyObject($v) { {
return is_object($v) && count(get_object_vars($v)) === 0; // 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 &&
* Throws an exception if the given value is not a valid @type value. property_exists($v, '@id'));
* }
* @param mixed $v the value to check.
*/ /**
protected static function _validateTypeValue($v) { * Returns true if the given value is a @value.
// must be a string or empty object *
if(is_string($v) || self::_isEmptyObject($v)) { * @param mixed $v the value to check.
return; *
} * @return bool true if the value is a @value, false if not.
*/
// must be an array protected static function _isValue($v)
$is_valid = false; {
if(is_array($v)) { // Note: A value is a @value if all of these hold true:
// must contain only strings // 1. It is an Object.
$is_valid = true; // 2. It has the @value property.
foreach($v as $e) { return is_object($v) && property_exists($v, '@value');
if(!(is_string($e))) { }
$is_valid = false;
break; /**
} * Returns true if the given value is a @list.
} *
} * @param mixed $v the value to check.
*
if(!$is_valid) { * @return bool true if the value is a @list, false if not.
throw new JsonLdException( */
'Invalid JSON-LD syntax; "@type" value must a string, an array ' . protected static function _isList($v)
'of strings, or an empty object.', {
'jsonld.SyntaxError', 'invalid type value', array('value' => $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 subject with properties.
* /**
* @param mixed $v the value to check. * Returns true if the given value is a blank node.
* *
* @return bool true if the value is a subject with properties, false if not. * @param mixed $v the value to check.
*/ *
protected static function _isSubject($v) { * @return bool true if the value is a blank node, false if not.
// Note: A value is a subject if all of these hold true: */
// 1. It is an Object. protected static function _isBlankNode($v)
// 2. It is not a @value, @set, or @list. {
// 3. It has more than 1 key OR any existing key is not @id. // Note: A value is a blank node if all of these hold true:
$rval = false; // 1. It is an Object.
if(is_object($v) && // 2. If it has an @id key its value begins with '_:'.
!property_exists($v, '@value') && // 3. It has no keys OR is not a @value, @set, or @list.
!property_exists($v, '@set') && $rval = false;
!property_exists($v, '@list')) { if (is_object($v)) {
$count = count(get_object_vars($v)); if (property_exists($v, '@id')) {
$rval = ($count > 1 || !property_exists($v, '@id')); $rval = (strpos($v->{'@id'}, '_:') === 0);
} } else {
return $rval; $rval = (count(get_object_vars($v)) === 0 ||
} !(property_exists($v, '@value') ||
property_exists($v, '@set') ||
/** property_exists($v, '@list')));
* Returns true if the given value is a subject reference. }
* }
* @param mixed $v the value to check. return $rval;
* }
* @return bool true if the value is a subject reference, false if not.
*/ /**
protected static function _isSubjectReference($v) { * Returns true if the given value is an absolute IRI, false if not.
// Note: A value is a subject reference if all of these hold true: *
// 1. It is an Object. * @param string $v the value to check.
// 2. It has a single key: @id. *
return (is_object($v) && count(get_object_vars($v)) === 1 && * @return bool true if the value is an absolute IRI, false if not.
property_exists($v, '@id')); */
} protected static function _isAbsoluteIri($v)
{
/** return strpos($v, ':') !== false;
* Returns true if the given value is a @value. }
*
* @param mixed $v the value to check. /**
* * Returns true if the given target has the given key and its
* @return bool true if the value is a @value, false if not. * value equals is the given value.
*/ *
protected static function _isValue($v) { * @param stdClass $target the target object.
// Note: A value is a @value if all of these hold true: * @param string key the key to check.
// 1. It is an Object. * @param mixed $value the value to check.
// 2. It has the @value property. *
return is_object($v) && property_exists($v, '@value'); * @return bool true if the target has the given key and its value matches.
} */
protected static function _hasKeyValue($target, $key, $value)
/** {
* Returns true if the given value is a @list. return (property_exists($target, $key) && $target->{$key} === $value);
* }
* @param mixed $v the value to check.
* /**
* @return bool true if the value is a @list, false if not. * 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.
protected static function _isList($v) { *
// Note: A value is a @list if all of these hold true: * @param stdClass $o1 the first object.
// 1. It is an Object. * @param stdClass $o2 the second object.
// 2. It has the @list property. * @param string key the key to check.
return is_object($v) && property_exists($v, '@list'); *
} * @return bool true if both objects have the same value for the key or
* neither has the key.
/** */
* Returns true if the given value is a blank node. protected static function _compareKeyValues($o1, $o2, $key)
* {
* @param mixed $v the value to check. if (property_exists($o1, $key)) {
* return property_exists($o2, $key) && $o1->{$key} === $o2->{$key};
* @return bool true if the value is a blank node, false if not. }
*/ return !property_exists($o2, $key);
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 '_:'. * Parses JSON and sets an appropriate exception message on error.
// 3. It has no keys OR is not a @value, @set, or @list. *
$rval = false; * @param string $json the JSON to parse.
if(is_object($v)) { *
if(property_exists($v, '@id')) { * @return mixed the parsed JSON object or array.
$rval = (strpos($v->{'@id'}, '_:') === 0); */
} else { protected static function _parse_json($json)
$rval = (count(get_object_vars($v)) === 0 || {
!(property_exists($v, '@value') || $rval = json_decode($json);
property_exists($v, '@set') || $error = json_last_error();
property_exists($v, '@list'))); if ($error === JSON_ERROR_NONE && $rval === null) {
} $error = JSON_ERROR_SYNTAX;
} }
return $rval; switch ($error) {
} case JSON_ERROR_NONE:
break;
/** case JSON_ERROR_DEPTH:
* Returns true if the given value is an absolute IRI, false if not. throw new JsonLdException(
* 'Could not parse JSON; the maximum stack depth has been exceeded.', 'jsonld.ParseError');
* @param string $v the value to check. case JSON_ERROR_STATE_MISMATCH:
* throw new JsonLdException(
* @return bool true if the value is an absolute IRI, false if not. 'Could not parse JSON; invalid or malformed JSON.', 'jsonld.ParseError');
*/ case JSON_ERROR_CTRL_CHAR:
protected static function _isAbsoluteIri($v) { case JSON_ERROR_SYNTAX:
return strpos($v, ':') !== false; throw new JsonLdException(
} 'Could not parse JSON; syntax error, malformed JSON.', 'jsonld.ParseError');
case JSON_ERROR_UTF8:
/** throw new JsonLdException(
* Returns true if the given target has the given key and its 'Could not parse JSON from URL; malformed UTF-8 characters.', 'jsonld.ParseError');
* value equals is the given value. default:
* throw new JsonLdException(
* @param stdClass $target the target object. 'Could not parse JSON from URL; unknown error.', 'jsonld.ParseError');
* @param string key the key to check. }
* @param mixed $value the value to check. return $rval;
* }
* @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 ?> */

1410
test.php
View file

@ -1,4 +1,5 @@
<?php <?php
/** /**
* PHP unit tests for JSON-LD. * PHP unit tests for JSON-LD.
* *
@ -8,756 +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
* @group json-ld.org * @group json-ld.org
* @dataProvider expandProvider * @dataProvider expandProvider
*/ */
public function testExpand($test) { public function testExpand($test)
$this->test = $test; {
$input = $test->readUrl('input'); $this->test = $test;
$options = $test->createOptions(); $input = $test->readUrl('input');
$test->run('jsonld_expand', array($input, $options)); $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
* @group json-ld.org * @group json-ld.org
* @dataProvider compactProvider * @dataProvider compactProvider
*/ */
public function testCompact($test) { public function testCompact($test)
$this->test = $test; {
$input = $test->readUrl('input'); $this->test = $test;
$context = $test->readProperty('context'); $input = $test->readUrl('input');
$options = $test->createOptions(); $context = $test->readProperty('context');
$test->run('jsonld_compact', array($input, $context, $options)); $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
* @group json-ld.org * @group json-ld.org
* @dataProvider flattenProvider * @dataProvider flattenProvider
*/ */
public function testFlatten($test) { public function testFlatten($test)
$this->test = $test; {
$input = $test->readUrl('input'); $this->test = $test;
$context = $test->readProperty('context'); $input = $test->readUrl('input');
$options = $test->createOptions(); $context = $test->readProperty('context');
$test->run('jsonld_flatten', array($input, $context, $options)); $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
* @group json-ld.org * @group json-ld.org
* @dataProvider toRdfProvider * @dataProvider toRdfProvider
*/ */
public function testToRdf($test) { public function testToRdf($test)
$this->test = $test; {
$input = $test->readUrl('input'); $this->test = $test;
$options = $test->createOptions(array('format' => 'application/nquads')); $input = $test->readUrl('input');
$test->run('jsonld_to_rdf', array($input, $options)); $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
* @group json-ld.org * @group json-ld.org
* @dataProvider fromRdfProvider * @dataProvider fromRdfProvider
*/ */
public function testFromRdf($test) { public function testFromRdf($test)
$this->test = $test; {
$input = $test->readProperty('input'); $this->test = $test;
$options = $test->createOptions(array('format' => 'application/nquads')); $input = $test->readProperty('input');
$test->run('jsonld_from_rdf', array($input, $options)); $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
* @group json-ld.org * @group json-ld.org
* @dataProvider frameProvider * @dataProvider frameProvider
*/ */
public function testFrame($test) { public function testFrame($test)
$this->test = $test; {
$input = $test->readUrl('input'); $this->test = $test;
$frame = $test->readProperty('frame'); $input = $test->readUrl('input');
$options = $test->createOptions(); $frame = $test->readProperty('frame');
$test->run('jsonld_frame', array($input, $frame, $options)); $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
* @group json-ld.org * @group json-ld.org
* @dataProvider normalizeProvider * @dataProvider normalizeProvider
*/ */
public function testNormalize($test) { public function testNormalize($test)
$this->test = $test; {
$input = $test->readUrl('input'); $this->test = $test;
$options = $test->createOptions(array('format' => 'application/nquads')); $input = $test->readUrl('input');
$test->run('jsonld_normalize', array($input, $options)); $options = $test->createOptions(['format' => 'application/nquads']);
} $test->run('jsonld_normalize', [$input, $options]);
}
/** /**
* Tests URGNA2012 normalization. * Tests URGNA2012 normalization.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group normalize * @group normalize
* @group normalization * @group normalization
* @dataProvider urgna2012Provider * @dataProvider urgna2012Provider
*/ */
public function testUrgna2012($test) { public function testUrgna2012($test)
$this->test = $test; {
$input = $test->readProperty('action'); $this->test = $test;
$options = $test->createOptions(array( $input = $test->readProperty('action');
'algorithm' => 'URGNA2012', $options = $test->createOptions([
'inputFormat' => 'application/nquads', 'algorithm' => 'URGNA2012',
'format' => 'application/nquads')); 'inputFormat' => 'application/nquads',
$test->run('jsonld_normalize', array($input, $options)); 'format' => 'application/nquads']);
} $test->run('jsonld_normalize', [$input, $options]);
}
/** /**
* Tests URDNA2015 normalization. * Tests URDNA2015 normalization.
* *
* @param JsonLdTest $test the test to run. * @param JsonLdTest $test the test to run.
* *
* @group normalize * @group normalize
* @group normalization * @group normalization
* @dataProvider urdna2015Provider * @dataProvider urdna2015Provider
*/ */
public function testUrdna2015($test) { public function testUrdna2015($test)
$this->test = $test; {
$input = $test->readProperty('action'); $this->test = $test;
$options = $test->createOptions(array( $input = $test->readProperty('action');
'algorithm' => 'URDNA2015', $options = $test->createOptions([
'inputFormat' => 'application/nquads', 'algorithm' => 'URDNA2015',
'format' => 'application/nquads')); 'inputFormat' => 'application/nquads',
$test->run('jsonld_normalize', array($input, $options)); 'format' => 'application/nquads']);
} $test->run('jsonld_normalize', [$input, $options]);
}
public function expandProvider() { public function expandProvider()
return new JsonLdTestIterator('jld:ExpandTest'); {
} return new JsonLdTestIterator('jld:ExpandTest');
}
public function compactProvider() { public function compactProvider()
return new JsonLdTestIterator('jld:CompactTest'); {
} return new JsonLdTestIterator('jld:CompactTest');
}
public function flattenProvider() { public function flattenProvider()
return new JsonLdTestIterator('jld:FlattenTest'); {
} return new JsonLdTestIterator('jld:FlattenTest');
}
public function toRdfProvider() { public function toRdfProvider()
return new JsonLdTestIterator('jld:ToRDFTest'); {
} return new JsonLdTestIterator('jld:ToRDFTest');
}
public function fromRdfProvider() { public function fromRdfProvider()
return new JsonLdTestIterator('jld:FromRDFTest'); {
} return new JsonLdTestIterator('jld:FromRDFTest');
}
public function normalizeProvider() { public function normalizeProvider()
return new JsonLdTestIterator('jld:NormalizeTest'); {
} return new JsonLdTestIterator('jld:NormalizeTest');
}
public function frameProvider() { public function frameProvider()
return new JsonLdTestIterator('jld:FrameTest'); {
} return new JsonLdTestIterator('jld:FrameTest');
}
public function urgna2012Provider() { public function urgna2012Provider()
return new JsonLdTestIterator('rdfn:Urgna2012EvalTest'); {
} return new JsonLdTestIterator('rdfn:Urgna2012EvalTest');
}
public function urdna2015Provider() { public function urdna2015Provider()
return new JsonLdTestIterator('rdfn:Urdna2015EvalTest'); {
} 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)
$entries = array_merge( {
JsonLdProcessor::getValues($this->data, 'sequence'), $entries = array_merge(
JsonLdProcessor::getValues($this->data, 'entries')); JsonLdProcessor::getValues($this->data, 'sequence'), JsonLdProcessor::getValues($this->data, 'entries'));
$includes = JsonLdProcessor::getValues($this->data, 'include'); $includes = JsonLdProcessor::getValues($this->data, 'include');
foreach($includes as $include) { foreach ($includes as $include) {
array_push($entries, $include . '.jsonld'); array_push($entries, $include . '.jsonld');
} }
foreach($entries as $entry) { foreach ($entries as $entry) {
if(is_string($entry)) { if (is_string($entry)) {
$filename = join( $filename = join(
DIRECTORY_SEPARATOR, array($this->dirname, $entry)); DIRECTORY_SEPARATOR, [$this->dirname, $entry]);
$entry = Util::readJson($filename); $entry = Util::readJson($filename);
} else { } else {
$filename = $this->filename; $filename = $this->filename;
} }
if(JsonLdProcessor::hasValue($entry, '@type', 'mf:Manifest') || if (JsonLdProcessor::hasValue($entry, '@type', 'mf:Manifest') ||
JsonLdProcessor::hasValue($entry, 'type', 'mf:Manifest')) { JsonLdProcessor::hasValue($entry, 'type', 'mf:Manifest')) {
// entry is another manifest // entry is another manifest
$manifest = new JsonLdManifest($entry, $filename); $manifest = new JsonLdManifest($entry, $filename);
$manifest->load($tests); $manifest->load($tests);
} else { } else {
// assume entry is a test // assume entry is a test
$test = new JsonLdTest($this, $entry, $filename); $test = new JsonLdTest($this, $entry, $filename);
$types = array_merge( $types = array_merge(
JsonLdProcessor::getValues($test->data, '@type'), JsonLdProcessor::getValues($test->data, '@type'), JsonLdProcessor::getValues($test->data, 'type'));
JsonLdProcessor::getValues($test->data, 'type')); foreach ($types as $type) {
foreach($types as $type) { if (!isset($tests[$type])) {
if(!isset($tests[$type])) { $tests[$type] = [];
$tests[$type] = array(); }
} $tests[$type][] = $test;
$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 = $this->filename = $filename;
JsonLdProcessor::hasValue( $this->dirname = dirname($filename);
$data, '@type', 'jld:PositiveEvaluationTest') || $this->isPositive = JsonLdProcessor::hasValue(
JsonLdProcessor::hasValue( $data, '@type', 'jld:PositiveEvaluationTest') ||
$data, 'type', 'jld:PositiveEvaluationTest'); JsonLdProcessor::hasValue(
$this->isNegative = $data, 'type', 'jld:PositiveEvaluationTest');
JsonLdProcessor::hasValue( $this->isNegative = JsonLdProcessor::hasValue(
$data, '@type', 'jld:NegativeEvaluationTest') || $data, '@type', 'jld:NegativeEvaluationTest') ||
JsonLdProcessor::hasValue( JsonLdProcessor::hasValue(
$data, 'type', 'jld:NegativeEvaluationTest'); $data, 'type', 'jld:NegativeEvaluationTest');
// generate test name // generate test name
if(isset($manifest->data->name)) { if (isset($manifest->data->name)) {
$manifestLabel = $manifest->data->name; $manifestLabel = $manifest->data->name;
} else if(isset($manifest->data->label)) { } else if (isset($manifest->data->label)) {
$manifestLabel = $manifest->data->label; $manifestLabel = $manifest->data->label;
} else { } else {
$manifestLabel = 'UNNAMED'; $manifestLabel = 'UNNAMED';
} }
if(isset($this->data->id)) { if (isset($this->data->id)) {
$testId = $this->data->id; $testId = $this->data->id;
} else { } else {
$testId = $this->data->{'@id'}; $testId = $this->data->{'@id'};
} }
if(isset($this->data->name)) { if (isset($this->data->name)) {
$testLabel = $this->data->name; $testLabel = $this->data->name;
} else if(isset($this->data->label)) { } else if (isset($this->data->label)) {
$testLabel = $this->data->label; $testLabel = $this->data->label;
} else { } else {
$testLabel = 'UNNAMED'; $testLabel = 'UNNAMED';
} }
$this->name = $manifestLabel . ' ' . $testId . ' - ' . $testLabel;
// expand @id and input base $this->name = $manifestLabel . ' ' . $testId . ' - ' . $testLabel;
if(isset($manifest->data->baseIri)) {
$data->{'@id'} = ($manifest->data->baseIri .
basename($manifest->filename) . $data->{'@id'});
$this->base = $manifest->data->baseIri . $data->input;
}
}
private function _getResultProperty() { // expand @id and input base
if(isset($this->data->expect)) { if (isset($manifest->data->baseIri)) {
return 'expect'; $data->{'@id'} = ($manifest->data->baseIri .
} else if(isset($this->data->result)) { basename($manifest->filename) . $data->{'@id'});
return 'result'; $this->base = $manifest->data->baseIri . $data->input;
} else { }
throw new Exception('No test result property found.'); }
}
}
public function run($fn, $params) { private function _getResultProperty()
// read expected data {
if($this->isNegative) { if (isset($this->data->expect)) {
$this->expected = $this->data->expect; return 'expect';
} else { } else if (isset($this->data->result)) {
$this->expected = $this->readProperty($this->_getResultProperty()); return 'result';
} } else {
throw new Exception('No test result property found.');
}
}
try { public function run($fn, $params)
$this->actual = call_user_func_array($fn, $params); {
if($this->isNegative) { // read expected data
throw new Exception('Expected an error; one was not raised.'); if ($this->isNegative) {
} $this->expected = $this->data->expect;
PHPUnit_Framework_TestCase::assertEquals($this->expected, $this->actual); } else {
} catch(Exception $e) { $this->expected = $this->readProperty($this->_getResultProperty());
// assume positive test }
if($this->isNegative) {
$this->actual = $this->getJsonLdErrorCode($e);
PHPUnit_Framework_TestCase::assertEquals(
$this->expected, $this->actual);
} else {
throw $e;
}
}
}
public function readUrl($property) { try {
if(!property_exists($this->data, $property)) { $this->actual = call_user_func_array($fn, $params);
return null; if ($this->isNegative) {
} throw new Exception('Expected an error; one was not raised.');
return $this->manifest->data->baseIri . $this->data->{$property}; }
} PHPUnit_Framework_TestCase::assertEquals($this->expected, $this->actual);
} catch (Exception $e) {
// assume positive test
if ($this->isNegative) {
$this->actual = $this->getJsonLdErrorCode($e);
PHPUnit_Framework_TestCase::assertEquals(
$this->expected, $this->actual);
} else {
throw $e;
}
}
}
public function readProperty($property) { public function readUrl($property)
$data = $this->data; {
if(!property_exists($data, $property)) { if (!property_exists($this->data, $property)) {
return null; return null;
} }
$filename = join( return $this->manifest->data->baseIri . $this->data->{$property};
DIRECTORY_SEPARATOR, array($this->dirname, $data->{$property})); }
$extension = pathinfo($filename, PATHINFO_EXTENSION);
if($extension === 'jsonld') {
return Util::readJson($filename);
}
return Util::readFile($filename);
}
public function createOptions($opts=array()) { public function readProperty($property)
$http_options = array( {
'contentType', 'httpLink', 'httpStatus', 'redirectTo'); $data = $this->data;
$test_options = (property_exists($this->data, 'option') ? if (!property_exists($data, $property)) {
$this->data->option : array()); return null;
$options = array(); }
foreach($test_options as $k => $v) { $filename = join(
if(!in_array($k, $http_options)) { DIRECTORY_SEPARATOR, [$this->dirname, $data->{$property}]);
$options[$k] = $v; $extension = pathinfo($filename, PATHINFO_EXTENSION);
} if ($extension === 'jsonld') {
} return Util::readJson($filename);
$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() { return Util::readFile($filename);
$base = 'http://json-ld.org/test-suite'; }
$test = $this;
$load_locally = function($url) use ($test, $base) { public function createOptions($opts = [])
$doc = (object)array( {
'contextUrl' => null, 'documentUrl' => $url, 'document' => null); $http_options = [
$options = (property_exists($test->data, 'option') ? 'contentType', 'httpLink', 'httpStatus', 'redirectTo'];
$test->data->option : null); $test_options = (property_exists($this->data, 'option') ?
if($options and $url === $test->base) { $this->data->option : []);
if(property_exists($options, 'redirectTo') && $options = [];
property_exists($options, 'httpStatus') && foreach ($test_options as $k => $v) {
$options->httpStatus >= '300') { if (!in_array($k, $http_options)) {
$doc->documentUrl = ($test->manifest->data->baseIri . $options[$k] = $v;
$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, array(
$ROOT_MANIFEST_DIR, $doc->documentUrl));
$doc->documentUrl = 'file://' . $filename;
} else {
$filename = join(
DIRECTORY_SEPARATOR, array(
$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) { $options['documentLoader'] = $this->createDocumentLoader();
// always load remote-doc and non-base tests remotely $options = array_merge($options, $opts);
if((strpos($url, $base) !== 0 && strpos($url, ':') !== false) || if (isset($options['expandContext'])) {
$test->manifest->data->name === 'Remote document') { $filename = join(
return call_user_func('jsonld_default_document_loader', $url); DIRECTORY_SEPARATOR, [$this->dirname, $options['expandContext']]);
} $options['expandContext'] = Util::readJson($filename);
}
// attempt to load locally return $options;
return call_user_func($load_locally, $url); }
};
return $local_loader; 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()) { }
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 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);