From 23cd99e8fb9f620b7f3965a52e0bfebf672cb6b0 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 27 Sep 2018 19:59:13 -0400 Subject: [PATCH] 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 --- composer.json | 2 +- jsonld.php | 11274 ++++++++++++++++++++++++------------------------ test.php | 1410 +++--- 3 files changed, 6385 insertions(+), 6301 deletions(-) diff --git a/composer.json b/composer.json index 63195ee..0f1c8d7 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ } ], "require": { - "php": ">=5.3.0", + "php": ">=5.4.0", "ext-json": "*" }, "autoload": { diff --git a/jsonld.php b/jsonld.php index 28b3e7c..06aafcd 100644 --- a/jsonld.php +++ b/jsonld.php @@ -1,4 +1,5 @@ compact($input, $ctx, $options); +function jsonld_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. */ -function jsonld_expand($input, $options=array()) { - $p = new JsonLdProcessor(); - return $p->expand($input, $options); +function jsonld_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. */ -function jsonld_flatten($input, $ctx, $options=array()) { - $p = new JsonLdProcessor(); - return $p->flatten($input, $ctx, $options); +function jsonld_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. */ -function jsonld_frame($input, $frame, $options=array()) { - $p = new JsonLdProcessor(); - return $p->frame($input, $frame, $options); +function jsonld_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. */ -function jsonld_link($input, $ctx, $options) { - // API matches running frame with a wildcard frame and embed: '@link' - // get arguments - $frame = new stdClass(); - if($ctx) { - $frame->{'@context'} = $ctx; - } - $frame->{'@embed'} = '@link'; - return jsonld_frame($input, $frame, $options); -}; +function jsonld_link($input, $ctx, $options) +{ + // API matches running frame with a wildcard frame and embed: '@link' + // get arguments + $frame = new stdClass(); + if ($ctx) { + $frame->{'@context'} = $ctx; + } + $frame->{'@embed'} = '@link'; + return jsonld_frame($input, $frame, $options); +} /** * Performs RDF dataset normalization on the given input. The input is @@ -146,9 +152,10 @@ function jsonld_link($input, $ctx, $options) { * * @return mixed the normalized output. */ -function jsonld_normalize($input, $options=array()) { - $p = new JsonLdProcessor(); - return $p->normalize($input, $options); +function jsonld_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. */ -function jsonld_from_rdf($input, $options=array()) { - $p = new JsonLdProcessor(); - return $p->fromRDF($input, $options); +function jsonld_from_rdf($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). */ -function jsonld_to_rdf($input, $options=array()) { - $p = new JsonLdProcessor(); - return $p->toRDF($input, $options); +function jsonld_to_rdf($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. */ -function jsonld_encode($input, $options=0, $depth=512) { - // newer PHP has a flag to avoid escaped '/' - 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)); +function jsonld_encode($input, $options = 0, $depth = 512) +{ + // newer PHP has a flag to avoid escaped '/' + 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)); } /** @@ -217,8 +227,9 @@ function jsonld_encode($input, $options=0, $depth=512) { * * @return mixed the resolved JSON-LD object, null on error. */ -function jsonld_decode($input) { - return json_decode($input); +function jsonld_decode($input) +{ + return json_decode($input); } /** @@ -241,35 +252,37 @@ function jsonld_decode($input) { * * @return assoc the parsed result. */ -function jsonld_parse_link_header($header) { - $rval = array(); - // split on unbracketed/unquoted commas - if(!preg_match_all( - '/(?:<[^>]*?>|"[^"]*?"|[^,])+/', $header, $entries, PREG_SET_ORDER)) { - return $rval; - } - $r_link_header = '/\s*<([^>]*?)>\s*(?:;\s*(.*))?/'; - foreach($entries as $entry) { - if(!preg_match($r_link_header, $entry[0], $match)) { - continue; - } - $result = (object)array('target' => $match[1]); - $params = $match[2]; - $r_params = '/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/'; - preg_match_all($r_params, $params, $matches, PREG_SET_ORDER); - 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; - } else if(is_array($rval[$rel])) { - $rval[$rel][] = $result; - } else { - $rval[$rel] = array($rval[$rel], $result); - } - } - return $rval; +function jsonld_parse_link_header($header) +{ + $rval = []; + // split on unbracketed/unquoted commas + if (!preg_match_all('/(?:<[^>]*?>|"[^"]*?"|[^,])+/', $header, $entries, PREG_SET_ORDER)) { + return $rval; + } + + $r_link_header = '/\s*<([^>]*?)>\s*(?:;\s*(.*))?/'; + foreach ($entries as $entry) { + if (!preg_match($r_link_header, $entry[0], $match)) { + continue; + } + $result = (object) ['target' => $match[1]]; + $params = $match[2]; + $r_params = '/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/'; + preg_match_all($r_params, $params, $matches, PREG_SET_ORDER); + 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; + } else if (is_array($rval[$rel])) { + $rval[$rel][] = $result; + } else { + $rval[$rel] = [$rval[$rel], $result]; + } + } + return $rval; } /** @@ -277,9 +290,10 @@ function jsonld_parse_link_header($header) { * * @param mixed input the JSON-LD input. */ -function jsonld_relabel_blank_nodes($input) { - $p = new JsonLdProcessor(); - return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input); +function jsonld_relabel_blank_nodes($input) +{ + $p = new JsonLdProcessor(); + return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input); } /** 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. */ -function jsonld_set_document_loader($load_document) { - global $jsonld_default_load_document; - $jsonld_default_load_document = $load_document; +function jsonld_set_document_loader($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. */ -function jsonld_get_url($url) { - global $jsonld_default_load_document; - if($jsonld_default_load_document !== null) { - $document_loader = $jsonld_default_load_document; - } else { - $document_loader = 'jsonld_default_document_loader'; - } +function jsonld_get_url($url) +{ + global $jsonld_default_load_document; + if ($jsonld_default_load_document !== null) { + $document_loader = $jsonld_default_load_document; + } else { + $document_loader = 'jsonld_default_document_loader'; + } - $remote_doc = call_user_func($document_loader, $url); - if($remote_doc) { - return $remote_doc->document; - } - return null; + $remote_doc = call_user_func($document_loader, $url); + if ($remote_doc) { + return $remote_doc->document; + } + + return null; } /** @@ -332,76 +349,80 @@ function jsonld_get_url($url) { * * @return stdClass the RemoteDocument object. */ -function jsonld_default_document_loader($url) { - $doc = (object)array( - 'contextUrl' => null, 'document' => null, 'documentUrl' => $url); - $redirects = array(); +function jsonld_default_document_loader($url) +{ + $doc = (object) [ + 'contextUrl' => null, 'document' => null, 'documentUrl' => $url]; + $redirects = []; - $opts = array( - 'http' => array( - 'method' => 'GET', - 'header' => - "Accept: application/ld+json\r\n"), - /* Note: Use jsonld_default_secure_document_loader for security. */ - 'ssl' => array( - 'verify_peer' => false, - 'allow_self_signed' => true) - ); + $opts = [ + 'http' => [ + 'method' => 'GET', + 'header' => + "Accept: application/ld+json\r\n"], + /* Note: Use jsonld_default_secure_document_loader for security. */ + 'ssl' => [ + 'verify_peer' => false, + 'allow_self_signed' => true] + ]; - $context = stream_context_create($opts); - $content_type = null; - stream_context_set_params($context, array('notification' => - function($notification_code, $severity, $message) use ( - &$redirects, &$content_type) { - switch($notification_code) { - case STREAM_NOTIFY_REDIRECTED: - $redirects[] = $message; - break; - case STREAM_NOTIFY_MIME_TYPE_IS: - $content_type = $message; - break; - }; - })); - $result = @file_get_contents($url, false, $context); - if($result === false) { - throw new JsonLdException( - 'Could not retrieve a JSON-LD document from the URL: ' . $url, - '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; - } + $context = stream_context_create($opts); + $content_type = null; + stream_context_set_params($context, ['notification' => + function($notification_code, $severity, $message) use ( + &$redirects, &$content_type) { + switch ($notification_code) { + case STREAM_NOTIFY_REDIRECTED: + $redirects[] = $message; + break; + case STREAM_NOTIFY_MIME_TYPE_IS: + $content_type = $message; + break; + }; + }]); + $result = @file_get_contents($url, false, $context); + if ($result === false) { + throw new JsonLdException( + 'Could not retrieve a JSON-LD document from the URL: ' . $url, 'jsonld.LoadDocumentError', 'loading document failed'); + } - // update document url based on redirects - $redirs = count($redirects); - if($redirs > 0) { - $url = $redirects[$redirs - 1]; - } - $doc->document = $result; - $doc->documentUrl = $url; - return $doc; + $link_header = []; + 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', ['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. */ -function jsonld_default_secure_document_loader($url) { - if(strpos($url, 'https') !== 0) { - throw new JsonLdException( - "Could not GET url: '$url'; 'https' is required.", - 'jsonld.LoadDocumentError', 'loading document failed'); - } +function jsonld_default_secure_document_loader($url) +{ + if (strpos($url, 'https') !== 0) { + throw new JsonLdException( + "Could not GET url: '$url'; 'https' is required.", + 'jsonld.LoadDocumentError', + 'loading document failed' + ); + } - $doc = (object)array( - 'contextUrl' => null, 'document' => null, 'documentUrl' => $url); - $redirects = array(); + $doc = (object) [ + 'contextUrl' => null, 'document' => null, 'documentUrl' => $url]; + $redirects = []; - // default JSON-LD https GET implementation - $opts = array( - 'http' => array( - 'method' => 'GET', - 'header' => - "Accept: application/ld+json\r\n"), - 'ssl' => array( - 'verify_peer' => true, - 'allow_self_signed' => false, - 'cafile' => '/etc/ssl/certs/ca-certificates.crt')); - $context = stream_context_create($opts); - $content_type = null; - stream_context_set_params($context, array('notification' => - function($notification_code, $severity, $message) use ( - &$redirects, &$content_type) { - switch($notification_code) { - case STREAM_NOTIFY_REDIRECTED: - $redirects[] = $message; - break; - case STREAM_NOTIFY_MIME_TYPE_IS: - $content_type = $message; - break; - }; - })); - $result = @file_get_contents($url, false, $context); - if($result === false) { - throw new JsonLdException( - 'Could not retrieve a JSON-LD document from the URL: ' + $url, - '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; - } + // default JSON-LD https GET implementation + $opts = [ + 'http' => [ + 'method' => 'GET', + 'header' => + "Accept: application/ld+json\r\n"], + 'ssl' => [ + 'verify_peer' => true, + 'allow_self_signed' => false, + 'cafile' => '/etc/ssl/certs/ca-certificates.crt']]; + $context = stream_context_create($opts); + $content_type = null; + stream_context_set_params($context, ['notification' => + function($notification_code, $severity, $message) use ( + &$redirects, &$content_type) { + switch ($notification_code) { + case STREAM_NOTIFY_REDIRECTED: + $redirects[] = $message; + break; + case STREAM_NOTIFY_MIME_TYPE_IS: + $content_type = $message; + break; + }; + }]); + $result = @file_get_contents($url, false, $context); + if ($result === false) { + throw new JsonLdException( + 'Could not retrieve a JSON-LD document from the URL: ' + $url, 'jsonld.LoadDocumentError', 'loading document failed'); + } - // 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; + $link_header = []; + 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', ['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. */ @@ -505,9 +533,10 @@ $jsonld_rdf_parsers = new stdClass(); * @param callable $parser(input) the parser function (takes a string as * a parameter and returns an RDF dataset). */ -function jsonld_register_rdf_parser($content_type, $parser) { - global $jsonld_rdf_parsers; - $jsonld_rdf_parsers->{$content_type} = $parser; +function jsonld_register_rdf_parser($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. */ -function jsonld_unregister_rdf_parser($content_type) { - global $jsonld_rdf_parsers; - if(property_exists($jsonld_rdf_parsers, $content_type)) { - unset($jsonld_rdf_parsers->{$content_type}); - } +function jsonld_unregister_rdf_parser($content_type) +{ + global $jsonld_rdf_parsers; + 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. */ -function jsonld_parse_url($url) { - if($url === null) { - $url = ''; - } +function jsonld_parse_url($url) +{ + if ($url === null) { + $url = ''; + } - $keys = array( - 'href', 'protocol', 'scheme', '?authority', 'authority', - '?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path', - '?query', 'query', '?fragment', 'fragment'); - $regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/"; - preg_match($regex, $url, $match); + $keys = [ + 'href', 'protocol', 'scheme', '?authority', 'authority', + '?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path', + '?query', 'query', '?fragment', 'fragment']; + $regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/"; + preg_match($regex, $url, $match); - $rval = array(); - $flags = array(); - $len = count($keys); - for($i = 0; $i < $len; ++$i) { - $key = $keys[$i]; - if(strpos($key, '?') === 0) { - $flags[substr($key, 1)] = !empty($match[$i]); - } else if(!isset($match[$i])) { - $rval[$key] = null; - } else { - $rval[$key] = $match[$i]; - } - } + $rval = []; + $flags = []; + $len = count($keys); + for ($i = 0; $i < $len; ++$i) { + $key = $keys[$i]; + if (strpos($key, '?') === 0) { + $flags[substr($key, 1)] = !empty($match[$i]); + } else if (!isset($match[$i])) { + $rval[$key] = null; + } else { + $rval[$key] = $match[$i]; + } + } - if(!$flags['authority']) { - $rval['authority'] = null; - } - if(!$flags['auth']) { - $rval['auth'] = $rval['user'] = $rval['pass'] = null; - } - if(!$flags['port']) { - $rval['port'] = null; - } - if(!$flags['query']) { - $rval['query'] = null; - } - if(!$flags['fragment']) { - $rval['fragment'] = null; - } + if (!$flags['authority']) { + $rval['authority'] = null; + } + if (!$flags['auth']) { + $rval['auth'] = $rval['user'] = $rval['pass'] = null; + } + if (!$flags['port']) { + $rval['port'] = null; + } + if (!$flags['query']) { + $rval['query'] = null; + } + if (!$flags['fragment']) { + $rval['fragment'] = null; + } - $rval['normalizedPath'] = jsonld_remove_dot_segments( - $rval['path'], !!$rval['authority']); + $rval['normalizedPath'] = jsonld_remove_dot_segments( + $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 bool $has_authority true if the URL has an authority, false if not. */ -function jsonld_remove_dot_segments($path, $has_authority) { - $rval = ''; +function jsonld_remove_dot_segments($path, $has_authority) +{ + $rval = ''; - if(strpos($path, '/') === 0) { - $rval = '/'; - } + if (strpos($path, '/') === 0) { + $rval = '/'; + } - // RFC 3986 5.2.4 (reworked) - $input = explode('/', $path); - $output = array(); - while(count($input) > 0) { - if($input[0] === '.' || ($input[0] === '' && count($input) > 1)) { - array_shift($input); - continue; - } - if($input[0] === '..') { - array_shift($input); - if($has_authority || - (count($output) > 0 && $output[count($output) - 1] !== '..')) { - array_pop($output); - } else { - // leading relative URL '..' - $output[] = '..'; - } - continue; - } - $output[] = array_shift($input); - } + // RFC 3986 5.2.4 (reworked) + $input = explode('/', $path); + $output = []; + while (count($input) > 0) { + if ($input[0] === '.' || ($input[0] === '' && count($input) > 1)) { + array_shift($input); + continue; + } + if ($input[0] === '..') { + array_shift($input); + if ($has_authority || + (count($output) > 0 && $output[count($output) - 1] !== '..')) { + array_pop($output); + } else { + // leading relative URL '..' + $output[] = '..'; + } + continue; + } + $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. */ -function jsonld_prepend_base($base, $iri) { - // skip IRI processing - if($base === null) { - return $iri; - } +function jsonld_prepend_base($base, $iri) +{ + // skip IRI processing + if ($base === null) { + return $iri; + } - // already an absolute IRI - if(strpos($iri, ':') !== false) { - return $iri; - } + // already an absolute IRI + if (strpos($iri, ':') !== false) { + return $iri; + } - // parse base if it is a string - if(is_string($base)) { - $base = jsonld_parse_url($base); - } + // parse base if it is a string + if (is_string($base)) { + $base = jsonld_parse_url($base); + } - // parse given IRI - $rel = jsonld_parse_url($iri); + // parse given IRI + $rel = jsonld_parse_url($iri); - // per RFC3986 5.2.2 - $transform = array('protocol' => $base['protocol']); + // per RFC3986 5.2.2 + $transform = ['protocol' => $base['protocol']]; - if($rel['authority'] !== null) { - $transform['authority'] = $rel['authority']; - $transform['path'] = $rel['path']; - $transform['query'] = $rel['query']; - } else { - $transform['authority'] = $base['authority']; + if ($rel['authority'] !== null) { + $transform['authority'] = $rel['authority']; + $transform['path'] = $rel['path']; + $transform['query'] = $rel['query']; + } else { + $transform['authority'] = $base['authority']; - if($rel['path'] === '') { - $transform['path'] = $base['path']; - if($rel['query'] !== null) { - $transform['query'] = $rel['query']; - } else { - $transform['query'] = $base['query']; - } - } else { - if(strpos($rel['path'], '/') === 0) { - // IRI represents an absolute path - $transform['path'] = $rel['path']; - } else { - // merge paths - $path = $base['path']; + if ($rel['path'] === '') { + $transform['path'] = $base['path']; + if ($rel['query'] !== null) { + $transform['query'] = $rel['query']; + } else { + $transform['query'] = $base['query']; + } + } else { + if (strpos($rel['path'], '/') === 0) { + // IRI represents an absolute path + $transform['path'] = $rel['path']; + } else { + // merge paths + $path = $base['path']; - // append relative path to the end of the last directory from base - if($rel['path'] !== '') { - $idx = strrpos($path, '/'); - $idx = ($idx === false) ? 0 : $idx + 1; - $path = substr($path, 0, $idx); - if(strlen($path) > 0 && substr($path, -1) !== '/') { - $path .= '/'; - } - $path .= $rel['path']; - } + // append relative path to the end of the last directory from base + if ($rel['path'] !== '') { + $idx = strrpos($path, '/'); + $idx = ($idx === false) ? 0 : $idx + 1; + $path = substr($path, 0, $idx); + if (strlen($path) > 0 && substr($path, -1) !== '/') { + $path .= '/'; + } + $path .= $rel['path']; + } - $transform['path'] = $path; - } - $transform['query'] = $rel['query']; - } - } + $transform['path'] = $path; + } + $transform['query'] = $rel['query']; + } + } - // remove slashes and dots in path - $transform['path'] = jsonld_remove_dot_segments( - $transform['path'], !!$transform['authority']); + // remove slashes and dots in path + $transform['path'] = jsonld_remove_dot_segments( + $transform['path'], !!$transform['authority']); - // construct URL - $rval = $transform['protocol']; - if($transform['authority'] !== null) { - $rval .= '//' . $transform['authority']; - } - $rval .= $transform['path']; - if($transform['query'] !== null) { - $rval .= '?' . $transform['query']; - } - if($rel['fragment'] !== null) { - $rval .= '#' . $rel['fragment']; - } + // construct URL + $rval = $transform['protocol']; + if ($transform['authority'] !== null) { + $rval .= '//' . $transform['authority']; + } + $rval .= $transform['path']; + if ($transform['query'] !== null) { + $rval .= '?' . $transform['query']; + } + if ($rel['fragment'] !== null) { + $rval .= '#' . $rel['fragment']; + } - // handle empty base - if($rval === '') { - $rval = './'; - } + // handle empty base + if ($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 * IRI. */ -function jsonld_remove_base($base, $iri) { - // skip IRI processing - if($base === null) { - return $iri; - } +function jsonld_remove_base($base, $iri) +{ + // skip IRI processing + if ($base === null) { + return $iri; + } - if(is_string($base)) { - $base = jsonld_parse_url($base); - } + if (is_string($base)) { + $base = jsonld_parse_url($base); + } - // establish base root - $root = ''; - if($base['href'] !== '') { - $root .= "{$base['protocol']}//{$base['authority']}"; - } else if(strpos($iri, '//') === false) { - // support network-path reference with empty base - $root .= '//'; - } + // establish base root + $root = ''; + if ($base['href'] !== '') { + $root .= "{$base['protocol']}//{$base['authority']}"; + } else if (strpos($iri, '//') === false) { + // support network-path reference with empty base + $root .= '//'; + } - // IRI not relative to base - if($root === '' || strpos($iri, $root) !== 0) { - return $iri; - } + // IRI not relative to base + if ($root === '' || strpos($iri, $root) !== 0) { + return $iri; + } - // remove root from IRI - $rel = jsonld_parse_url(substr($iri, strlen($root))); + // remove root from IRI + $rel = jsonld_parse_url(substr($iri, strlen($root))); - // remove path segments that match (do not remove last segment unless there - // is a hash or query) - $base_segments = explode('/', $base['normalizedPath']); - $iri_segments = explode('/', $rel['normalizedPath']); - $last = ($rel['query'] || $rel['fragment']) ? 0 : 1; - while(count($base_segments) > 0 && count($iri_segments) > $last) { - if($base_segments[0] !== $iri_segments[0]) { - break; - } - array_shift($base_segments); - array_shift($iri_segments); - } + // remove path segments that match (do not remove last segment unless there + // is a hash or query) + $base_segments = explode('/', $base['normalizedPath']); + $iri_segments = explode('/', $rel['normalizedPath']); + $last = ($rel['query'] || $rel['fragment']) ? 0 : 1; + while (count($base_segments) > 0 && count($iri_segments) > $last) { + if ($base_segments[0] !== $iri_segments[0]) { + break; + } + array_shift($base_segments); + array_shift($iri_segments); + } - // use '../' for each non-matching base segment - $rval = ''; - if(count($base_segments) > 0) { - // 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) - array_pop($base_segments); - foreach($base_segments as $segment) { - $rval .= '../'; - } - } + // use '../' for each non-matching base segment + $rval = ''; + if (count($base_segments) > 0) { + // 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) + array_pop($base_segments); + foreach ($base_segments as $segment) { + $rval .= '../'; + } + } - // prepend remaining segments - $rval .= implode('/', $iri_segments); + // prepend remaining segments + $rval .= implode('/', $iri_segments); - // add query and hash - if($rel['query'] !== null) { - $rval .= "?{$rel['query']}"; - } - if($rel['fragment'] !== null) { - $rval .= "#{$rel['fragment']}"; - } + // add query and hash + if ($rel['query'] !== null) { + $rval .= "?{$rel['query']}"; + } + if ($rel['fragment'] !== null) { + $rval .= "#{$rel['fragment']}"; + } - if($rval === '') { - $rval = './'; - } + if ($rval === '') { + $rval = './'; + } - return $rval; + return $rval; } - /** * A JSON-LD processor. */ -class JsonLdProcessor { - /** XSD constants */ - const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean'; - const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double'; - const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer'; - const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string'; - - /** RDF constants */ - 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'; - const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'; - const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'; - 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; - - /** Processor-specific RDF dataset parsers. */ - protected $rdfParsers = null; - - /** - * Constructs a JSON-LD processor. - */ - 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. - * [base] the base IRI to use. - * [compactArrays] true to compact arrays to single values when - * appropriate, false not to (default: true). - * [graph] true to always output a top-level graph (default: false). - * [skipExpansion] true to assume the input is expanded and skip - * expansion, false not to, defaults to false. - * [activeCtx] true to also return the active context used. - * [documentLoader(url)] the document loader. - * - * @return mixed the compacted JSON-LD output. - */ - public function compact($input, $ctx, $options) { - global $jsonld_default_load_document; - - if($ctx === null) { - throw new JsonLdException( - 'The compaction context must not be null.', - 'jsonld.CompactError', 'invalid local context'); - } - - // nothing to compact - if($input === null) { - return null; - } - - self::setdefaults($options, array( - 'base' => is_string($input) ? $input : '', - 'compactArrays' => true, - 'graph' => false, - 'skipExpansion' => false, - 'activeCtx' => false, - 'documentLoader' => $jsonld_default_load_document, - 'link' => false)); - if($options['link']) { - // force skip expansion when linking, "link" is not part of the - // public API, it should only be called from framing - $options['skipExpansion'] = true; - } - - if($options['skipExpansion'] === true) { - $expanded = $input; - } else { - // expand input - try { - $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); - } 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); - - if($options['compactArrays'] && - !$options['graph'] && is_array($compacted)) { - if(count($compacted) === 1) { - // simplify to a single item - $compacted = $compacted[0]; - } else if(count($compacted) === 0) { - // simplify to an empty object - $compacted = new stdClass(); - } - } 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'}; - } - - // build output context - $ctx = self::copy($ctx); - $ctx = self::arrayify($ctx); - - // remove empty contexts - $tmp = $ctx; - $ctx = array(); - 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); - if($ctx_length === 1) { - $ctx = $ctx[0]; - } - - // add context and/or @graph - if(is_array($compacted)) { - // use '@graph' keyword - $kwgraph = $this->_compactIri($active_ctx, '@graph'); - $graph = $compacted; - $compacted = new stdClass(); - if($has_context) { - $compacted->{'@context'} = $ctx; - } - $compacted->{$kwgraph} = $graph; - } else if(is_object($compacted) && $has_context) { - // reorder keys so @context is first - $graph = $compacted; - $compacted = new stdClass(); - $compacted->{'@context'} = $ctx; - foreach($graph as $k => $v) { - $compacted->{$k} = $v; - } - } - - if($options['activeCtx']) { - return array('compacted' => $compacted, 'activeCtx' => $active_ctx); - } - - return $compacted; - } - - /** - * Performs JSON-LD expansion. - * - * @param mixed $input the JSON-LD object to expand. - * @param assoc $options the options to use: - * [base] the base IRI to use. - * [expandContext] a context to expand with. - * [keepFreeFloatingNodes] true to keep free-floating nodes, - * false not to, defaults to false. - * [documentLoader(url)] the document loader. - * - * @return array the expanded JSON-LD output. - */ - public function expand($input, $options) { - global $jsonld_default_load_document; - self::setdefaults($options, array( - 'keepFreeFloatingNodes' => false, - 'documentLoader' => $jsonld_default_load_document)); - - // if input is a string, attempt to dereference remote document - if(is_string($input)) { - $remote_doc = call_user_func($options['documentLoader'], $input); - } else { - $remote_doc = (object)array( - 'contextUrl' => null, - 'documentUrl' => null, - 'document' => $input); - } - - try { - if($remote_doc->document === null) { - throw new JsonLdException( - 'No remote document found at the given URL.', - 'jsonld.NullRemoteDocument'); - } - if(is_string($remote_doc->document)) { - $remote_doc->document = self::_parse_json($remote_doc->document); - } - } catch(Exception $e) { - throw new JsonLdException( - 'Could not retrieve a JSON-LD document from the URL.', - 'jsonld.LoadDocumentError', 'loading document failed', - array('remoteDoc' => $remote_doc), $e); - } - - // set default base - self::setdefault($options, 'base', $remote_doc->documentUrl ?: ''); - - // build meta-object and retrieve all @context urls - $input = (object)array( - 'document' => self::copy($remote_doc->document), - 'remoteContext' => (object)array( - '@context' => $remote_doc->contextUrl)); - if(isset($options['expandContext'])) { - $expand_context = self::copy($options['expandContext']); - if(is_object($expand_context) && - property_exists($expand_context, '@context')) { - $input->expandContext = $expand_context; - } else { - $input->expandContext = (object)array('@context' => $expand_context); - } - } - - // retrieve all @context URLs in the input - try { - $this->_retrieveContextUrls( - $input, new stdClass(), $options['documentLoader'], $options['base']); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not perform JSON-LD expansion.', - 'jsonld.ExpandError', null, null, $e); - } - - $active_ctx = $this->_getInitialContext($options); - $document = $input->document; - $remote_context = $input->remoteContext->{'@context'}; - - // process optional expandContext - if(property_exists($input, 'expandContext')) { - $active_ctx = self::_processContext( - $active_ctx, $input->expandContext, $options); - } - - // process remote context from HTTP Link Header - if($remote_context) { - $active_ctx = self::_processContext( - $active_ctx, $remote_context, $options); - } - - // do expansion - $expanded = $this->_expand($active_ctx, null, $document, $options, false); - - // optimize away @graph with no other properties - if(is_object($expanded) && property_exists($expanded, '@graph') && - count(array_keys((array)$expanded)) === 1) { - $expanded = $expanded->{'@graph'}; - } else if($expanded === null) { - $expanded = array(); - } - // normalize to an array - return self::arrayify($expanded); - } - - /** - * Performs JSON-LD flattening. - * - * @param mixed $input the JSON-LD to flatten. - * @param ctx the context to use to compact the flattened output, or null. - * @param assoc $options the options to use: - * [base] the base IRI to use. - * [expandContext] a context to expand with. - * [documentLoader(url)] the document loader. - * - * @return array the flattened output. - */ - public function flatten($input, $ctx, $options) { - global $jsonld_default_load_document; - self::setdefaults($options, array( - 'base' => is_string($input) ? $input : '', - 'documentLoader' => $jsonld_default_load_document)); - - try { - // expand input - $expanded = $this->expand($input, $options); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not expand input before flattening.', - 'jsonld.FlattenError', null, null, $e); - } - - // do flattening - $flattened = $this->_flatten($expanded); - - if($ctx === null) { - return $flattened; - } - - // compact result (force @graph option to true, skip expansion) - $options['graph'] = true; - $options['skipExpansion'] = true; - try { - $compacted = $this->compact($flattened, $ctx, $options); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not compact flattened output.', - 'jsonld.FlattenError', null, null, $e); - } - - return $compacted; - } - - /** - * Performs JSON-LD framing. - * - * @param mixed $input the JSON-LD object to frame. - * @param stdClass $frame the JSON-LD frame to use. - * @param $options the framing options. - * [base] the base IRI to use. - * [expandContext] a context to expand with. - * [embed] default @embed flag: '@last', '@always', '@never', '@link' - * (default: '@last'). - * [explicit] default @explicit flag (default: false). - * [requireAll] default @requireAll flag (default: true). - * [omitDefault] default @omitDefault flag (default: false). - * [documentLoader(url)] the document loader. - * - * @return stdClass the framed JSON-LD output. - */ - public function frame($input, $frame, $options) { - global $jsonld_default_load_document; - self::setdefaults($options, array( - 'base' => is_string($input) ? $input : '', - 'compactArrays' => true, - 'embed' => '@last', - 'explicit' => false, - 'requireAll' => true, - 'omitDefault' => false, - 'documentLoader' => $jsonld_default_load_document)); - - // if frame is a string, attempt to dereference remote document - if(is_string($frame)) { - $remote_frame = call_user_func($options['documentLoader'], $frame); - } else { - $remote_frame = (object)array( - 'contextUrl' => null, - 'documentUrl' => null, - 'document' => $frame); - } - - try { - if($remote_frame->document === null) { - throw new JsonLdException( - '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); - } - } catch(Exception $e) { - throw new JsonLdException( - 'Could not retrieve a JSON-LD document from the URL.', - 'jsonld.LoadDocumentError', 'loading document failed', - array('remoteDoc' => $remote_frame), $e); - } - - // preserve frame context - $frame = $remote_frame->document; - if($frame !== null) { - $ctx = (property_exists($frame, '@context') ? - $frame->{'@context'} : new stdClass()); - if($remote_frame->contextUrl !== null) { - if($ctx !== null) { - $ctx = $remote_frame->contextUrl; - } else { - $ctx = self::arrayify($ctx); - $ctx[] = $remote_frame->contextUrl; - } - $frame->{'@context'} = $ctx; - } - } - - try { - // expand input - $expanded = $this->expand($input, $options); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not expand input before framing.', - 'jsonld.FrameError', null, null, $e); - } - - try { - // expand frame - $opts = $options; - $opts['keepFreeFloatingNodes'] = true; - $expanded_frame = $this->expand($frame, $opts); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not expand frame before framing.', - 'jsonld.FrameError', null, null, $e); - } - - // do framing - $framed = $this->_frame($expanded, $expanded_frame, $options); - - try { - // compact result (force @graph option to true, skip expansion, check - // for linked embeds) - $options['graph'] = true; - $options['skipExpansion'] = true; - $options['link'] = new ArrayObject(); - $options['activeCtx'] = true; - $result = $this->compact($framed, $ctx, $options); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not compact framed output.', - 'jsonld.FrameError', null, null, $e); - } - - $compacted = $result['compacted']; - $active_ctx = $result['activeCtx']; - - // get graph alias - $graph = $this->_compactIri($active_ctx, '@graph'); - // remove @preserve from results - $options['link'] = new ArrayObject(); - $compacted->{$graph} = $this->_removePreserve( - $active_ctx, $compacted->{$graph}, $options); - return $compacted; - } - - /** - * Performs JSON-LD normalization. - * - * @param mixed $input the JSON-LD object to normalize. - * @param assoc $options the options to use: - * [base] the base IRI to use. - * [expandContext] a context to expand with. - * [inputFormat] the format if input is not JSON-LD: - * 'application/nquads' for N-Quads. - * [format] the format if output is a string: - * 'application/nquads' for N-Quads. - * [documentLoader(url)] the document loader. - * - * @return mixed the normalized output. - */ - public function normalize($input, $options) { - global $jsonld_default_load_document; - self::setdefaults($options, array( - 'base' => is_string($input) ? $input : '', - 'documentLoader' => $jsonld_default_load_document)); - - if(isset($options['inputFormat'])) { - if($options['inputFormat'] != 'application/nquads') { - throw new JsonLdException( - 'Unknown normalization input format.', 'jsonld.NormalizeError'); - } - $dataset = $this->parseNQuads($input); - } else { - try { - // convert to RDF dataset then do normalization - $opts = $options; - if(isset($opts['format'])) { - unset($opts['format']); - } - $opts['produceGeneralizedRdf'] = false; - $dataset = $this->toRDF($input, $opts); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not convert input to RDF dataset before normalization.', - 'jsonld.NormalizeError', null, null, $e); - } - } - - // do normalization - return $this->_normalize($dataset, $options); - } - - /** - * Converts an RDF dataset to JSON-LD. - * - * @param mixed $dataset a serialized string of RDF in a format specified - * 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). - * [useRdfType] true to use rdf:type, false to use @type - * (default: false). - * [useNativeTypes] true to convert XSD types into native types - * (boolean, integer, double), false not to (default: false). - * - * @return array the JSON-LD output. - */ - public function fromRDF($dataset, $options) { - global $jsonld_rdf_parsers; - - self::setdefaults($options, array( - 'useRdfType' => false, - 'useNativeTypes' => false)); - - if(!isset($options['format']) && is_string($dataset)) { - // set default format to nquads - $options['format'] = 'application/nquads'; - } - - // handle special format - if(isset($options['format']) && $options['format']) { - // supported formats (processor-specific and global) - if(($this->rdfParsers !== null && - !property_exists($this->rdfParsers, $options['format'])) || - $this->rdfParsers === null && - !property_exists($jsonld_rdf_parsers, $options['format'])) { - throw new JsonLdException( - 'Unknown input format.', - 'jsonld.UnknownFormat', null, array('format' => $options['format'])); - } - if($this->rdfParsers !== null) { - $callable = $this->rdfParsers->{$options['format']}; - } else { - $callable = $jsonld_rdf_parsers->{$options['format']}; - } - $dataset = call_user_func($callable, $dataset); - } - - // convert from RDF - return $this->_fromRDF($dataset, $options); - } - - /** - * Outputs the RDF dataset found in the given JSON-LD object. - * - * @param mixed $input the JSON-LD object. - * @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: - * 'application/nquads' for N-Quads. - * [produceGeneralizedRdf] true to output generalized RDF, false - * to produce only standard RDF (default: false). - * [documentLoader(url)] the document loader. - * - * @return mixed the resulting RDF dataset (or a serialization of it). - */ - public function toRDF($input, $options) { - global $jsonld_default_load_document; - self::setdefaults($options, array( - 'base' => is_string($input) ? $input : '', - 'produceGeneralizedRdf' => false, - 'documentLoader' => $jsonld_default_load_document)); - - try { - // expand input - $expanded = $this->expand($input, $options); - } catch(JsonLdException $e) { - throw new JsonLdException( - 'Could not expand input before serialization to RDF.', - 'jsonld.RdfError', null, null, $e); - } - - // create node map for default graph (and any named graphs) - $namer = new UniqueNamer('_:b'); - $node_map = (object)array('@default' => new stdClass()); - $this->_createNodeMap($expanded, $node_map, '@default', $namer); - - // output RDF dataset - $dataset = new stdClass(); - $graph_names = array_keys((array)$node_map); - sort($graph_names); - foreach($graph_names as $graph_name) { - $graph = $node_map->{$graph_name}; - // skip relative IRIs - if($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) { - $dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options); - } - } - - $rval = $dataset; - - // convert to output format - if(isset($options['format']) && $options['format']) { - // supported formats - if($options['format'] === 'application/nquads') { - $rval = self::toNQuads($dataset); - } else { - throw new JsonLdException( - 'Unknown output format.', 'jsonld.UnknownFormat', - null, array('format' => $options['format'])); - } - } - - return $rval; - } - - /** - * 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. - * @param mixed $local_ctx the local context to process. - * @param assoc $options the options to use: - * [documentLoader(url)] the document loader. - * - * @return stdClass the new active context. - */ - public function processContext($active_ctx, $local_ctx, $options) { - global $jsonld_default_load_document; - self::setdefaults($options, array( - 'base' => '', - 'documentLoader' => $jsonld_default_load_document)); - - // return initial context early for null context - if($local_ctx === null) { - return $this->_getInitialContext($options); - } - - // retrieve URLs in local_ctx - $local_ctx = self::copy($local_ctx); - if(is_string($local_ctx) or ( - is_object($local_ctx) && !property_exists($local_ctx, '@context'))) { - $local_ctx = (object)array('@context' => $local_ctx); - } - try { - $this->_retrieveContextUrls( - $local_ctx, new stdClass(), - $options['documentLoader'], $options['base']); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not process JSON-LD context.', - 'jsonld.ContextError', null, null, $e); - } - - // process context - return $this->_processContext($active_ctx, $local_ctx, $options); - } - - /** - * Returns true if the given subject has the given property. - * - * @param stdClass $subject the subject to check. - * @param string $property the property to look for. - * - * @return bool true if the subject has the given property, false if not. - */ - public static function hasProperty($subject, $property) { - $rval = false; - if(property_exists($subject, $property)) { - $value = $subject->{$property}; - $rval = (!is_array($value) || count($value) > 0); - } - return $rval; - } - - /** - * Determines if the given value is a property of the given subject. - * - * @param stdClass $subject the subject to check. - * @param string $property the property to check. - * @param mixed $value the value to check. - * - * @return bool true if the value exists, false if not. - */ - public static function hasValue($subject, $property, $value) { - $rval = false; - if(self::hasProperty($subject, $property)) { - $val = $subject->{$property}; - $is_list = self::_isList($val); - if(is_array($val) || $is_list) { - if($is_list) { - $val = $val->{'@list'}; - } - foreach($val as $v) { - if(self::compareValues($value, $v)) { - $rval = true; - break; - } - } - } else if(!is_array($value)) { - // avoid matching the set of values with an array value parameter - $rval = self::compareValues($value, $val); - } - } - return $rval; - } - - /** - * Adds a value to a subject. If the value is an array, all values in the - * array will be added. - * - * 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. - * - * @param stdClass $subject the subject to add the value to. - * @param string $property the property that relates the value to the subject. - * @param mixed $value the value to add. - * @param assoc [$options] the options to use: - * [propertyIsArray] true if the property is always an array, false - * if not (default: false). - * [allowDuplicate] true to allow duplicates, false not to (uses a - * simple shallow comparison of subject ID or value) - * (default: true). - */ - public static function addValue( - $subject, $property, $value, $options=array()) { - self::setdefaults($options, array( - 'allowDuplicate' => true, - 'propertyIsArray' => false)); - - if(is_array($value)) { - if(count($value) === 0 && $options['propertyIsArray'] && - !property_exists($subject, $property)) { - $subject->{$property} = array(); - } - foreach($value as $v) { - self::addValue($subject, $property, $v, $options); - } - } else if(property_exists($subject, $property)) { - // check if subject already has value if duplicates not allowed - $has_value = (!$options['allowDuplicate'] && - self::hasValue($subject, $property, $value)); - - // make property an array if value not present or always an array - if(!is_array($subject->{$property}) && - (!$has_value || $options['propertyIsArray'])) { - $subject->{$property} = array($subject->{$property}); - } - - // add new value - if(!$has_value) { - $subject->{$property}[] = $value; - } - } else { - // add new value as set or single value - $subject->{$property} = ($options['propertyIsArray'] ? - array($value) : $value); - } - } - - /** - * Gets all of the values for a subject's property as an array. - * - * @param stdClass $subject the subject. - * @param string $property the property. - * - * @return array all of the values for a subject's property as an array. - */ - public static function getValues($subject, $property) { - $rval = (property_exists($subject, $property) ? - $subject->{$property} : array()); - return self::arrayify($rval); - } - - /** - * Removes a property from a subject. - * - * @param stdClass $subject the subject. - * @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. - * @param mixed $value the value to remove. - * @param assoc [$options] the options to use: - * [propertyIsArray] true if the property is always an array, - * false if not (default: false). - */ - public static function removeValue( - $subject, $property, $value, $options=array()) { - self::setdefaults($options, array( - 'propertyIsArray' => false)); - - // filter out value - $filter = function($e) use ($value) { - return !self::compareValues($e, $value); - }; - $values = self::getValues($subject, $property); - $values = array_values(array_filter($values, $filter)); - - if(count($values) === 0) { - self::removeProperty($subject, $property); - } else if(count($values) === 1 && !$options['propertyIsArray']) { - $subject->{$property} = $values[0]; - } else { - $subject->{$property} = $values; - } - } - - /** - * Compares two JSON-LD values for equality. Two JSON-LD values will be - * considered equal if: - * - * 1. They are both primitives of the same type and value. - * 2. They are both @values with the same @value, @type, @language, - * and @index, OR - * 3. They both have @ids that are the same. - * - * @param mixed $v1 the first value. - * @param mixed $v2 the second value. - * - * @return bool true if v1 and v2 are considered equal, false if not. - */ - public static function compareValues($v1, $v2) { - // 1. equal primitives - if($v1 === $v2) { - return true; - } - - // 2. equal @values - if(self::_isValue($v1) && self::_isValue($v2)) { - return ( - self::_compareKeyValues($v1, $v2, '@value') && - self::_compareKeyValues($v1, $v2, '@type') && - self::_compareKeyValues($v1, $v2, '@language') && - self::_compareKeyValues($v1, $v2, '@index')); - } - - // 3. equal @ids - if(is_object($v1) && property_exists($v1, '@id') && - is_object($v2) && property_exists($v2, '@id')) { - return $v1->{'@id'} === $v2->{'@id'}; - } - - return false; - } - - /** - * 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 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. - */ - public static function getContextValue($ctx, $key, $type) { - $rval = null; - - // return null for invalid key - if($key === null) { - return $rval; - } - - // get default language - if($type === '@language' && property_exists($ctx, $type)) { - $rval = $ctx->{$type}; - } - - // get specific entry information - if(property_exists($ctx->mappings, $key)) { - $entry = $ctx->mappings->{$key}; - if($entry === null) { - return null; - } - - if($type === null) { - // return whole entry - $rval = $entry; - } else if(property_exists($entry, $type)) { - // return entry value for type - $rval = $entry->{$type}; - } - } - - return $rval; - } - - /** - * Parses RDF in the form of N-Quads. - * - * @param string $input the N-Quads input to parse. - * - * @return stdClass an RDF dataset. - */ - public static function parseNQuads($input) { - // define partial regexes - $iri = '(?:<([^:]+:[^>]*)>)'; - $bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))'; - $plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"'; - $datatype = "(?:\\^\\^$iri)"; - $language = '(?:@([a-z]+(?:-[a-z0-9]+)*))'; - $literal = "(?:$plain(?:$datatype|$language)?)"; - $ws = '[ \\t]'; - $eoln = '/(?:\r\n)|(?:\n)|(?:\r)/'; - $empty = "/^$ws*$/"; - - // define quad part regexes - $subject = "(?:$iri|$bnode)$ws+"; - $property = "$iri$ws+"; - $object = "(?:$iri|$bnode|$literal)$ws*"; - $graph_name = "(?:\\.|(?:(?:$iri|$bnode)$ws*\\.))"; - - // full quad regex - $quad = "/^$ws*$subject$property$object$graph_name$ws*$/"; - - // build RDF dataset - $dataset = new stdClass(); - - // split N-Quad input into lines - $lines = preg_split($eoln, $input); - $line_number = 0; - foreach($lines as $line) { - $line_number += 1; - - // skip empty lines - if(preg_match($empty, $line)) { - continue; - } - - // parse quad - if(!preg_match($quad, $line, $match)) { - throw new JsonLdException( - 'Error while parsing N-Quads; invalid quad.', - 'jsonld.ParseError', null, array('line' => $line_number)); - } - - // create RDF triple - $triple = (object)array( - 'subject' => new stdClass(), - 'predicate' => new stdClass(), - 'object' => new stdClass()); - - // get subject - if($match[1] !== '') { - $triple->subject->type = 'IRI'; - $triple->subject->value = $match[1]; - } else { - $triple->subject->type = 'blank node'; - $triple->subject->value = $match[2]; - } - - // get predicate - $triple->predicate->type = 'IRI'; - $triple->predicate->value = $match[3]; - - // get object - if($match[4] !== '') { - $triple->object->type = 'IRI'; - $triple->object->value = $match[4]; - } else if($match[5] !== '') { - $triple->object->type = 'blank node'; - $triple->object->value = $match[5]; - } else { - $triple->object->type = 'literal'; - $unescaped = str_replace( - array('\"', '\t', '\n', '\r', '\\\\'), - array('"', "\t", "\n", "\r", '\\'), - $match[6]); - if(isset($match[7]) && $match[7] !== '') { - $triple->object->datatype = $match[7]; - } else if(isset($match[8]) && $match[8] !== '') { - $triple->object->datatype = self::RDF_LANGSTRING; - $triple->object->language = $match[8]; - } else { - $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] !== '') { - $name = $match[9]; - } else if(isset($match[10]) && $match[10] !== '') { - $name = $match[10]; - } - - // initialize graph in dataset - if(!property_exists($dataset, $name)) { - $dataset->{$name} = array($triple); - } else { - // add triple if unique to its graph - $unique = true; - $triples = &$dataset->{$name}; - foreach($triples as $t) { - if(self::_compareRDFTriples($t, $triple)) { - $unique = false; - break; - } - } - if($unique) { - $triples[] = $triple; - } - } - } - - return $dataset; - } - - /** - * Converts an RDF dataset to N-Quads. - * - * @param stdClass $dataset the RDF dataset to convert. - * - * @return string the N-Quads string. - */ - public static function toNQuads($dataset) { - $quads = array(); - foreach($dataset as $graph_name => $triples) { - foreach($triples as $triple) { - if($graph_name === '@default') { - $graph_name = null; - } - $quads[] = self::toNQuad($triple, $graph_name); - } - } - sort($quads); - return implode($quads); - } - - /** - * 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 - * for the default graph. - * @param string $bnode the bnode the quad is mapped to (optional, for - * use during normalization only). - * - * @return string the N-Quad string. - */ - public static function toNQuad($triple, $graph_name, $bnode=null) { - $s = $triple->subject; - $p = $triple->predicate; - $o = $triple->object; - $g = $graph_name; - - $quad = ''; - - // subject is an IRI - if($s->type === 'IRI') { - $quad .= "<{$s->value}>"; - } else if($bnode !== null) { - // bnode normalization mode - $quad .= ($s->value === $bnode) ? '_:a' : '_:z'; - } else { - // bnode normal mode - $quad .= $s->value; - } - $quad .= ' '; - - // predicate is an IRI - if($p->type === 'IRI') { - $quad .= "<{$p->value}>"; - } else if($bnode !== null) { - // FIXME: TBD what to do with bnode predicates during normalization - // bnode normalization mode - $quad .= '_:p'; - } else { - // bnode normal mode - $quad .= $p->value; - } - $quad .= ' '; - - // object is IRI, bnode, or literal - if($o->type === 'IRI') { - $quad .= "<{$o->value}>"; - } else if($o->type === 'blank node') { - if($bnode !== null) { - // normalization mode - $quad .= ($o->value === $bnode) ? '_:a' : '_:z'; - } else { - // normal mode - $quad .= $o->value; - } - } else { - $escaped = str_replace( - array('\\', "\t", "\n", "\r", '"'), - array('\\\\', '\t', '\n', '\r', '\"'), - $o->value); - $quad .= '"' . $escaped . '"'; - if($o->datatype === self::RDF_LANGSTRING) { - if($o->language) { - $quad .= "@{$o->language}"; - } - } else if($o->datatype !== self::XSD_STRING) { - $quad .= "^^<{$o->datatype}>"; - } - } - - // graph - if($g !== null) { - if(strpos($g, '_:') !== 0) { - $quad .= " <$g>"; - } else if($bnode) { - $quad .= ' _:g'; - } else { - $quad .= " $g"; - } - } - - $quad .= " .\n"; - return $quad; - } - - /** - * Registers a processor-specific RDF dataset parser by content-type. - * Global parsers will no longer be used by this processor. - * - * @param string $content_type the content-type for the parser. - * @param callable $parser(input) the parser function (takes a string as - * a parameter and returns an RDF dataset). - */ - public function registerRDFParser($content_type, $parser) { - if($this->rdfParsers === null) { - $this->rdfParsers = new stdClass(); - } - $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 - * parsers will be re-enabled. - * - * @param string $content_type the content-type for the parser. - */ - public function unregisterRDFParser($content_type) { - if($this->rdfParsers !== null && - property_exists($this->rdfParsers, $content_type)) { - unset($this->rdfParsers->{$content_type}); - if(count(get_object_vars($content_type)) === 0) { - $this->rdfParsers = null; - } - } - } - - /** - * If $value is an array, returns $value, otherwise returns an array - * containing $value as the only element. - * - * @param mixed $value the value. - * - * @return array an array. - */ - public static function arrayify($value) { - return is_array($value) ? $value : array($value); - } - - /** - * Clones an object, array, or string/number. - * - * @param mixed $value the value to clone. - * - * @return mixed the cloned value. - */ - public static function copy($value) { - if(is_object($value) || is_array($value)) { - return unserialize(serialize($value)); - } - return $value; - } - - /** - * 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 mixed $value the value to set. - */ - 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. - * @param assoc $defaults the default keys and values. - */ - 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. - * @param mixed $active_property the compacted property with the element - * to compact, null for none. - * @param mixed $element the element to compact. - * @param assoc $options the compaction options. - * - * @return mixed the compacted value. - */ - protected function _compact( - $active_ctx, $active_property, $element, $options) { - // recursively compact array - if(is_array($element)) { - $rval = array(); - foreach($element as $e) { - // compact, dropping any null values - $compacted = $this->_compact( - $active_ctx, $active_property, $e, $options); - if($compacted !== null) { - $rval[] = $compacted; - } - } - if($options['compactArrays'] && count($rval) === 1) { - // use single element if no container is specified - $container = self::getContextValue( - $active_ctx, $active_property, '@container'); - if($container === null) { - $rval = $rval[0]; - } - } - return $rval; - } - - // recursively compact object - if(is_object($element)) { - if($options['link'] && property_exists($element, '@id') && - isset($options['link'][$element->{'@id'}])) { - // 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 - if(!isset($options['link'][$element->{'@id'}])) { - $options['link'][$element->{'@id'}] = array(); - } - $options['link'][$element->{'@id'}][] = array( - 'expanded' => $element, 'compacted' => $rval); - } - return $rval; - } - - // FIXME: avoid misuse of active property as an expanded property? - $inside_reverse = ($active_property === '@reverse'); - - $rval = new stdClass(); - - if($options['link'] && property_exists($element, '@id')) { - // store linked element - if(!isset($options['link'][$element->{'@id'}])) { - $options['link'][$element->{'@id'}] = array(); - } - $options['link'][$element->{'@id'}][] = array( - 'expanded' => $element, 'compacted' => $rval); - } - - // process element keys in order - $keys = array_keys((array)$element); - sort($keys); - foreach($keys as $expanded_property) { - $expanded_value = $element->{$expanded_property}; - - // compact @id and @type(s) - if($expanded_property === '@id' || $expanded_property === '@type') { - if(is_string($expanded_value)) { - // compact single @id - $compacted_value = $this->_compactIri( - $active_ctx, $expanded_value, null, - array('vocab' => ($expanded_property === '@type'))); - } else { - // expanded value must be a @type array - $compacted_value = array(); - foreach($expanded_value as $ev) { - $compacted_value[] = $this->_compactIri( - $active_ctx, $ev, null, array('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); - self::addValue( - $rval, $alias, $compacted_value, - array('propertyIsArray' => $is_array)); - continue; - } - - // handle @reverse - if($expanded_property === '@reverse') { - // recursively compact expanded value - $compacted_value = $this->_compact( - $active_ctx, '@reverse', $expanded_value, $options); - - // handle double-reversed properties - foreach($compacted_value as $compacted_property => $value) { - if(property_exists($active_ctx->mappings, $compacted_property) && - $active_ctx->mappings->{$compacted_property} && - $active_ctx->mappings->{$compacted_property}->reverse) { - $container = self::getContextValue( - $active_ctx, $compacted_property, '@container'); - $use_array = ($container === '@set' || - !$options['compactArrays']); - self::addValue( - $rval, $compacted_property, $value, - array('propertyIsArray' => $use_array)); - unset($compacted_value->{$compacted_property}); - } - } - - if(count(array_keys((array)$compacted_value)) > 0) { - // use keyword alias and add value - $alias = $this->_compactIri($active_ctx, $expanded_property); - self::addValue($rval, $alias, $compacted_value); - } - - continue; - } - - // handle @index property - if($expanded_property === '@index') { - // drop @index if inside an @index container - $container = self::getContextValue( - $active_ctx, $active_property, '@container'); - if($container === '@index') { - continue; - } - - // use keyword alias and add value - $alias = $this->_compactIri($active_ctx, $expanded_property); - 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' && - self::_isKeyword($expanded_property)) { - // use keyword alias and add value as is - $alias = $this->_compactIri($active_ctx, $expanded_property); - self::addValue($rval, $alias, $expanded_value); - continue; - } - - // Note: expanded value must be an array due to expansion algorithm. - - // preserve empty arrays - if(count($expanded_value) === 0) { - $item_active_property = $this->_compactIri( - $active_ctx, $expanded_property, $expanded_value, - array('vocab' => true), $inside_reverse); - self::addValue( - $rval, $item_active_property, array(), - array('propertyIsArray' => true)); - } - - // recusively process array values - foreach($expanded_value as $expanded_item) { - // compact property and get container type - $item_active_property = $this->_compactIri( - $active_ctx, $expanded_property, $expanded_item, - array('vocab' => true), $inside_reverse); - $container = self::getContextValue( - $active_ctx, $item_active_property, '@container'); - - // get @list value if appropriate - $is_list = self::_isList($expanded_item); - $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); - - // handle @list - if($is_list) { - // ensure @list value is an array - $compacted_item = self::arrayify($compacted_item); - - if($container !== '@list') { - // wrap using @list alias - $compacted_item = (object)array( - $this->_compactIri($active_ctx, '@list') => $compacted_item); - - // include @index from expanded @list, if any - if(property_exists($expanded_item, '@index')) { - $compacted_item->{$this->_compactIri($active_ctx, '@index')} = - $expanded_item->{'@index'}; - } - } else if(property_exists($rval, $item_active_property)) { - // can't use @list container for more than 1 list - 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 ' . - 'unwanted items into the list.', 'jsonld.SyntaxError', - 'compaction to list of lists'); - } - } - - // handle language and index maps - if($container === '@language' || $container === '@index') { - // get or create the map object - if(property_exists($rval, $item_active_property)) { - $map_object = $rval->{$item_active_property}; - } else { - $rval->{$item_active_property} = $map_object = new stdClass(); - } - - // if container is a language map, simplify compacted value to - // a simple string - if($container === '@language' && self::_isValue($compacted_item)) { - $compacted_item = $compacted_item->{'@value'}; - } - - // add compact value to map object using key from expanded value - // based on the container type - self::addValue( - $map_object, $expanded_item->{$container}, $compacted_item); - } else { - // use an array if: compactArrays flag is false, - // @container is @set or @list, value is an empty - // array, or key is @graph - $is_array = (!$options['compactArrays'] || - $container === '@set' || $container === '@list' || - (is_array($compacted_item) && count($compacted_item) === 0) || - $expanded_property === '@list' || - $expanded_property === '@graph'); - - // add compact value - self::addValue( - $rval, $item_active_property, $compacted_item, - array('propertyIsArray' => $is_array)); - } - } - } - - return $rval; - } - - // only primitives remain which are already compact - return $element; - } - - /** - * Recursively expands an element using the given context. Any context in - * the element will be removed. All context URLs must have been retrieved - * before calling this method. - * - * @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. - * @param assoc $options the expansion options. - * @param bool $inside_list true if the property is a list, false if not. - * - * @return mixed the expanded value. - */ - protected function _expand( - $active_ctx, $active_property, $element, $options, $inside_list) { - // nothing to expand - if($element === null) { - return $element; - } - - // recursively expand array - if(is_array($element)) { - $rval = array(); - $container = self::getContextValue( - $active_ctx, $active_property, '@container'); - $inside_list = $inside_list || $container === '@list'; - foreach($element as $e) { - // expand element - $e = $this->_expand( - $active_ctx, $active_property, $e, $options, $inside_list); - if($inside_list && (is_array($e) || self::_isList($e))) { - // lists of lists are illegal - throw new JsonLdException( - 'Invalid JSON-LD syntax; lists of lists are not permitted.', - 'jsonld.SyntaxError', 'list of lists'); - } - // drop null values - if($e !== null) { - if(is_array($e)) { - $rval = array_merge($rval, $e); - } else { - $rval[] = $e; - } - } - } - return $rval; - } - - if(!is_object($element)) { - // drop free-floating scalars that are not in lists - if(!$inside_list && - ($active_property === null || - $this->_expandIri($active_ctx, $active_property, - array('vocab' => true)) === '@graph')) { - return null; - } - - // expand element according to value expansion rules - return $this->_expandValue($active_ctx, $active_property, $element); - } - - // recursively expand object: - - // if element has a context, process it - if(property_exists($element, '@context')) { - $active_ctx = $this->_processContext( - $active_ctx, $element->{'@context'}, $options); - } - - // expand the active property - $expanded_active_property = $this->_expandIri( - $active_ctx, $active_property, array('vocab' => true)); - - $rval = new stdClass(); - $keys = array_keys((array)$element); - sort($keys); - foreach($keys as $key) { - $value = $element->{$key}; - - if($key === '@context') { - continue; - } - - // expand key to IRI - $expanded_property = $this->_expandIri( - $active_ctx, $key, array('vocab' => true)); - - // drop non-absolute IRI keys that aren't keywords - if($expanded_property === null || - !(self::_isAbsoluteIri($expanded_property) || - self::_isKeyword($expanded_property))) { - continue; - } - - 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 ' . - 'property.', 'jsonld.SyntaxError', 'invalid reverse property map', - array('value' => $value)); - } - if(property_exists($rval, $expanded_property)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; colliding keywords detected.', - 'jsonld.SyntaxError', 'colliding keywords', - array('keyword' => $expanded_property)); - } - } - - // syntax error if @id is not a string - if($expanded_property === '@id' && !is_string($value)) { - if(!isset($options['isFrame']) || !$options['isFrame']) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@id" value must a string.', - 'jsonld.SyntaxError', 'invalid @id value', - array('value' => $value)); - } - if(!is_object($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@id" value must a string or an object.', - 'jsonld.SyntaxError', 'invalid @id value', - array('value' => $value)); - } - } - - // validate @type value - if($expanded_property === '@type') { - $this->_validateTypeValue($value); - } - - // @graph must be an array or an object - if($expanded_property === '@graph' && - !(is_object($value) || is_array($value))) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@graph" value must not be an ' . - 'object or an array.', 'jsonld.SyntaxError', - 'invalid @graph value', array('value' => $value)); - } - - // @value must not be an object or an array - if($expanded_property === '@value' && - (is_object($value) || is_array($value))) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@value" value must not be an ' . - 'object or an array.', 'jsonld.SyntaxError', - 'invalid value object value', array('value' => $value)); - } - - // @language must be a string - if($expanded_property === '@language') { - if($value === null) { - // drop null @language values, they expand as if they didn't exist - continue; - } - if(!is_string($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@language" value must not be a string.', - 'jsonld.SyntaxError', 'invalid language-tagged string', - array('value' => $value)); - } - // ensure language value is lowercase - $value = strtolower($value); - } - - // @index must be a string - if($expanded_property === '@index') { - if(!is_string($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@index" value must be a string.', - 'jsonld.SyntaxError', 'invalid @index value', - array('value' => $value)); - } - } - - // @reverse must be an object - if($expanded_property === '@reverse') { - if(!is_object($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@reverse" value must be an object.', - 'jsonld.SyntaxError', 'invalid @reverse value', - array('value' => $value)); - } - - $expanded_value = $this->_expand( - $active_ctx, '@reverse', $value, $options, $inside_list); - - // properties double-reversed - if(property_exists($expanded_value, '@reverse')) { - foreach($expanded_value->{'@reverse'} as $rproperty => $rvalue) { - self::addValue( - $rval, $rproperty, $rvalue, array('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'}; - } else { - $reverse_map = null; - } - foreach($expanded_value as $property => $items) { - if($property === '@reverse') { - continue; - } - if($reverse_map === null) { - $reverse_map = $rval->{'@reverse'} = new stdClass(); - } - self::addValue( - $reverse_map, $property, array(), - array('propertyIsArray' => true)); - foreach($items as $item) { - 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', - array('value' => $expanded_value)); - } - self::addValue( - $reverse_map, $property, $item, - array('propertyIsArray' => true)); - } - } - - continue; - } - - $container = self::getContextValue($active_ctx, $key, '@container'); - - if($container === '@language' && is_object($value)) { - // handle language map container (skip if value is not an object) - $expanded_value = $this->_expandLanguageMap($value); - } else if($container === '@index' && is_object($value)) { - // handle index container (skip if value is not an object) - $expanded_value = array(); - $value_keys = array_keys((array)$value); - sort($value_keys); - foreach($value_keys as $value_key) { - $val = $value->{$value_key}; - $val = self::arrayify($val); - $val = $this->_expand($active_ctx, $key, $val, $options, false); - foreach($val as $item) { - if(!property_exists($item, '@index')) { - $item->{'@index'} = $value_key; - } - $expanded_value[] = $item; - } - } - } else { - // recurse into @list or @set - $is_list = ($expanded_property === '@list'); - if($is_list || $expanded_property === '@set') { - $next_active_property = $active_property; - if($is_list && $expanded_active_property === '@graph') { - $next_active_property = null; - } - $expanded_value = $this->_expand( - $active_ctx, $next_active_property, $value, $options, $is_list); - if($is_list && self::_isList($expanded_value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; lists of lists are not permitted.', - 'jsonld.SyntaxError', 'list of lists'); - } - } else { - // recursively expand value with key as new active property - $expanded_value = $this->_expand( - $active_ctx, $key, $value, $options, false); - } - } - - // drop null values if property is not @value - if($expanded_value === null && $expanded_property !== '@value') { - continue; - } - - // convert expanded value to @list if container specifies it - if($expanded_property !== '@list' && !self::_isList($expanded_value) && - $container === '@list') { - // ensure expanded value is an array - $expanded_value = (object)array( - '@list' => self::arrayify($expanded_value)); - } - - // FIXME: can this be merged with code above to simplify? - // merge in reverse properties - if(property_exists($active_ctx->mappings, $key) && - $active_ctx->mappings->{$key} && - $active_ctx->mappings->{$key}->reverse) { - if(property_exists($rval, '@reverse')) { - $reverse_map = $rval->{'@reverse'}; - } else { - $reverse_map = $rval->{'@reverse'} = new stdClass(); - } - $expanded_value = self::arrayify($expanded_value); - foreach($expanded_value as $item) { - 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', - array('value' => $expanded_value)); - } - self::addValue( - $reverse_map, $expanded_property, $item, - array('propertyIsArray' => true)); - } - continue; - } - - // add value for property - // use an array except for certain keywords - $use_array = (!in_array( - $expanded_property, array( - '@index', '@id', '@type', '@value', '@language'))); - self::addValue( - $rval, $expanded_property, $expanded_value, - array('propertyIsArray' => $use_array)); - } - - // get property count on expanded output - $keys = array_keys((array)$rval); - $count = count($keys); - - // @value must only have @language or @type - if(property_exists($rval, '@value')) { - // @value must only have @language or @type - if(property_exists($rval, '@type') && - property_exists($rval, '@language')) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; an element containing "@value" may not ' . - 'contain both "@type" and "@language".', - 'jsonld.SyntaxError', 'invalid value object', - array('element' => $rval)); - } - $valid_count = $count - 1; - if(property_exists($rval, '@type')) { - $valid_count -= 1; - } - if(property_exists($rval, '@index')) { - $valid_count -= 1; - } - if(property_exists($rval, '@language')) { - $valid_count -= 1; - } - if($valid_count !== 0) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; an element containing "@value" may only ' . - 'have an "@index" property and at most one other property ' . - 'which can be "@type" or "@language".', - 'jsonld.SyntaxError', 'invalid value object', - array('element' => $rval)); - } - // drop null @values - if($rval->{'@value'} === null) { - $rval = null; - } else if(property_exists($rval, '@language') && - !is_string($rval->{'@value'})) { - // if @language is present, @value must be a string - throw new JsonLdException( - 'Invalid JSON-LD syntax; only strings may be language-tagged.', - 'jsonld.SyntaxError', 'invalid language-tagged value', - array('element' => $rval)); - } else if(property_exists($rval, '@type') && - (!self::_isAbsoluteIri($rval->{'@type'}) || - strpos($rval->{'@type'}, '_:') === 0)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; an element containing "@value" ' . - 'and "@type" must have an absolute IRI for the value ' . - 'of "@type".', 'jsonld.SyntaxError', 'invalid typed value', - array('element' => $rval)); - } - } else if(property_exists($rval, '@type') && !is_array($rval->{'@type'})) { - // convert @type to an array - $rval->{'@type'} = array($rval->{'@type'}); - } else if(property_exists($rval, '@set') || - property_exists($rval, '@list')) { - // handle @set and @list - if($count > 1 && !($count === 2 && property_exists($rval, '@index'))) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; if an element has the property "@set" ' . - 'or "@list", then it can have at most one other property that is ' . - '"@index".', 'jsonld.SyntaxError', 'invalid set or list object', - array('element' => $rval)); - } - // optimize away @set - if(property_exists($rval, '@set')) { - $rval = $rval->{'@set'}; - $keys = array_keys((array)$rval); - $count = count($keys); - } - } else if($count === 1 && property_exists($rval, '@language')) { - // drop objects with only @language - $rval = null; - } - - // drop certain top-level objects that do not occur in lists - if(is_object($rval) && - !$options['keepFreeFloatingNodes'] && !$inside_list && - ($active_property === null || $expanded_active_property === '@graph')) { - // drop empty object or top-level @value/@list, or object with only @id - if($count === 0 || property_exists($rval, '@value') || - property_exists($rval, '@list') || - ($count === 1 && property_exists($rval, '@id'))) { - $rval = null; - } - } - - return $rval; - } - - /** - * Performs JSON-LD flattening. - * - * @param array $input the expanded JSON-LD to flatten. - * - * @return array the flattened output. - */ - protected function _flatten($input) { - // produce a map of all subjects and name each bnode - $namer = new UniqueNamer('_:b'); - $graphs = (object)array('@default' => new stdClass()); - $this->_createNodeMap($input, $graphs, '@default', $namer); - - // add all non-default graphs to default graph - $default_graph = $graphs->{'@default'}; - $graph_names = array_keys((array)$graphs); - foreach($graph_names as $graph_name) { - if($graph_name === '@default') { - continue; - } - $node_map = $graphs->{$graph_name}; - if(!property_exists($default_graph, $graph_name)) { - $default_graph->{$graph_name} = (object)array( - '@id' => $graph_name, '@graph' => array()); - } - $subject = $default_graph->{$graph_name}; - if(!property_exists($subject, '@graph')) { - $subject->{'@graph'} = array(); - } - $ids = array_keys((array)$node_map); - sort($ids); - foreach($ids as $id) { - $node = $node_map->{$id}; - // only add full subjects - if(!self::_isSubjectReference($node)) { - $subject->{'@graph'}[] = $node; - } - } - } - - // produce flattened output - $flattened = array(); - $keys = array_keys((array)$default_graph); - sort($keys); - foreach($keys as $key) { - $node = $default_graph->{$key}; - // only add full subjects to top-level - if(!self::_isSubjectReference($node)) { - $flattened[] = $node; - } - } - return $flattened; - } - - /** - * Performs JSON-LD framing. - * - * @param array $input the expanded JSON-LD to frame. - * @param array $frame the expanded JSON-LD frame to use. - * @param assoc $options the framing options. - * - * @return array the framed output. - */ - protected function _frame($input, $frame, $options) { - // create framing state - $state = (object)array( - 'options' => $options, - 'graphs' => (object)array( - '@default' => new stdClass(), - '@merged' => new stdClass()), - 'subjectStack' => array(), - 'link' => new stdClass()); - - // produce a map of all graphs and name each bnode - // FIXME: currently uses subjects from @merged graph only - $namer = new UniqueNamer('_:b'); - $this->_createNodeMap($input, $state->graphs, '@merged', $namer); - $state->subjects = $state->graphs->{'@merged'}; - - // frame the subjects - $framed = new ArrayObject(); - $keys = array_keys((array)$state->subjects); - sort($keys); - $this->_matchFrame($state, $keys, $frame, $framed, null); - return (array)$framed; - } - - /** - * Performs normalization on the given RDF dataset. - * - * @param stdClass $dataset the RDF dataset to normalize. - * @param assoc $options the normalization options. - * - * @return mixed the normalized output. - */ - protected function _normalize($dataset, $options) { - // create quads and map bnodes to their associated quads - $quads = array(); - $bnodes = new stdClass(); - foreach($dataset as $graph_name => $triples) { - if($graph_name === '@default') { - $graph_name = null; - } - foreach($triples as $triple) { - $quad = $triple; - if($graph_name !== null) { - if(strpos($graph_name, '_:') === 0) { - $quad->name = (object)array( - 'type' => 'blank node', 'value' => $graph_name); - } else { - $quad->name = (object)array( - 'type' => 'IRI', 'value' => $graph_name); - } - } - $quads[] = $quad; - - foreach(array('subject', 'object', 'name') as $attr) { - if(property_exists($quad, $attr) && - $quad->{$attr}->type === 'blank node') { - $id = $quad->{$attr}->value; - if(property_exists($bnodes, $id)) { - $bnodes->{$id}->quads[] = $quad; - } else { - $bnodes->{$id} = (object)array('quads' => array($quad)); - } - } - } - } - } - - // mapping complete, start canonical naming - $namer = new UniqueNamer('_:c14n'); - - // continue to hash bnode quads while bnodes are assigned names - $unnamed = null; - $nextUnnamed = array_keys((array)$bnodes); - $duplicates = null; - do { - $unnamed = $nextUnnamed; - $nextUnnamed = array(); - $duplicates = new stdClass(); - $unique = new stdClass(); - foreach($unnamed as $bnode) { - // hash quads for each unnamed bnode - $hash = $this->_hashQuads($bnode, $bnodes, $namer); - - // store hash as unique or a duplicate - if(property_exists($duplicates, $hash)) { - $duplicates->{$hash}[] = $bnode; - $nextUnnamed[] = $bnode; - } else if(property_exists($unique, $hash)) { - $duplicates->{$hash} = array($unique->{$hash}, $bnode); - $nextUnnamed[] = $unique->{$hash}; - $nextUnnamed[] = $bnode; - unset($unique->{$hash}); - } else { - $unique->{$hash} = $bnode; - } - } - - // name unique bnodes in sorted hash order - $hashes = array_keys((array)$unique); - sort($hashes); - foreach($hashes as $hash) { - $namer->getName($unique->{$hash}); - } - } - while(count($unnamed) > count($nextUnnamed)); - - // enumerate duplicate hash groups in sorted order - $hashes = array_keys((array)$duplicates); - sort($hashes); - foreach($hashes as $hash) { - // process group - $group = $duplicates->{$hash}; - $results = array(); - foreach($group as $bnode) { - // skip already-named bnodes - if($namer->isNamed($bnode)) { - continue; - } - - // hash bnode paths - $path_namer = new UniqueNamer('_:b'); - $path_namer->getName($bnode); - $results[] = $this->_hashPaths($bnode, $bnodes, $namer, $path_namer); - } - - // name bnodes in hash order - usort($results, function($a, $b) { - $a = $a->hash; - $b = $b->hash; - return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); - }); - foreach($results as $result) { - // name all bnodes in path namer in key-entry order - foreach($result->pathNamer->order as $bnode) { - $namer->getName($bnode); - } - } - } - - // create normalized array - $normalized = array(); - - /* Note: At this point all bnodes in the set of RDF quads have been - assigned canonical names, which have been stored in the 'namer' object. - Here each quad is updated by assigning each of its bnodes its new name - via the 'namer' object. */ - - // update bnode names in each quad and serialize - foreach($quads as $quad) { - foreach(array('subject', 'object', 'name') as $attr) { - if(property_exists($quad, $attr) && - $quad->{$attr}->type === 'blank node' && - strpos($quad->{$attr}->value, '_:c14n') !== 0) { - $quad->{$attr}->value = $namer->getName($quad->{$attr}->value); - } - } - $normalized[] = $this->toNQuad($quad, property_exists($quad, 'name') ? - $quad->name->value : null); - } - - // sort normalized output - sort($normalized); - - // handle output format - if(isset($options['format']) && $options['format']) { - if($options['format'] === 'application/nquads') { - return implode($normalized); - } - throw new JsonLdException( - 'Unknown output format.', - 'jsonld.UnknownFormat', null, array('format' => $options['format'])); - } - - // return RDF dataset - return $this->parseNQuads(implode($normalized)); - } - - /** - * Converts an RDF dataset to JSON-LD. - * - * @param stdClass $dataset the RDF dataset. - * @param assoc $options the RDF serialization options. - * - * @return array the JSON-LD output. - */ - protected function _fromRDF($dataset, $options) { - $default_graph = new stdClass(); - $graph_map = (object)array('@default' => $default_graph); - $referenced_once = (object)array(); - - foreach($dataset as $name => $graph) { - if(!property_exists($graph_map, $name)) { - $graph_map->{$name} = new stdClass(); - } - if($name !== '@default' && !property_exists($default_graph, $name)) { - $default_graph->{$name} = (object)array('@id' => $name); - } - $node_map = $graph_map->{$name}; - foreach($graph as $triple) { - // get subject, predicate, object - $s = $triple->subject->value; - $p = $triple->predicate->value; - $o = $triple->object; - - if(!property_exists($node_map, $s)) { - $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)) { - $node_map->{$o->value} = (object)array('@id' => $o->value); - } - - if($p === self::RDF_TYPE && !$options['useRdfType'] && $object_is_id) { - self::addValue( - $node, '@type', $o->value, array('propertyIsArray' => true)); - continue; - } - - $value = self::_RDFToObject($o, $options['useNativeTypes']); - self::addValue($node, $p, $value, array('propertyIsArray' => true)); - - // object may be an RDF list/partial list node but we can't know - // easily until all triples are read - if($object_is_id) { - if($o->value === self::RDF_NIL) { - $object = $node_map->{$o->value}; - if(!property_exists($object, 'usages')) { - $object->usages = array(); - } - $object->usages[] = (object)array( - 'node' => $node, - 'property' => $p, - 'value' => $value); - } else if(property_exists($referenced_once, $o->value)) { - // object referenced more than once - $referenced_once->{$o->value} = false; - } else { - // track single reference - $referenced_once->{$o->value} = (object)array( - 'node' => $node, - 'property' => $p, - 'value' => $value); - } - } - } - } - - // convert linked lists to @list arrays - foreach($graph_map as $name => $graph_object) { - // no @lists to be converted, continue - if(!property_exists($graph_object, self::RDF_NIL)) { - continue; - } - - // iterate backwards through each RDF list - $nil = $graph_object->{self::RDF_NIL}; - foreach($nil->usages as $usage) { - $node = $usage->node; - $property = $usage->property; - $head = $usage->value; - $list = array(); - $list_nodes = array(); - - // ensure node is a well-formed list node; it must: - // 1. Be referenced only once. - // 2. Have an array for rdf:first that has 1 item. - // 3. Have an array for rdf:rest that has 1 item. - // 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)); - while($property === self::RDF_REST && - property_exists($referenced_once, $node->{'@id'}) && - is_object($referenced_once->{$node->{'@id'}}) && - property_exists($node, self::RDF_FIRST) && - property_exists($node, self::RDF_REST) && - is_array($node->{self::RDF_FIRST}) && - is_array($node->{self::RDF_REST}) && - count($node->{self::RDF_FIRST}) === 1 && - count($node->{self::RDF_REST}) === 1 && - ($node_key_count === 3 || ($node_key_count === 4 && - 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'}; - - // get next node, moving backwards through list - $usage = $referenced_once->{$node->{'@id'}}; - $node = $usage->node; - $property = $usage->property; - $head = $usage->value; - $node_key_count = count(array_keys((array)$node)); - - // if node is not a blank node, then list head found - if(strpos($node->{'@id'}, '_:') !== 0) { - break; - } - } - - // list is nested in another list - if($property === self::RDF_FIRST) { - // 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 - continue; - } - - // preserve list head - $head = $graph_object->{$head->{'@id'}}->{self::RDF_REST}[0]; - array_pop($list); - array_pop($list_nodes); - } - - // transform list into @list object - unset($head->{'@id'}); - $head->{'@list'} = array_reverse($list); - foreach($list_nodes as $list_node) { - unset($graph_object->{$list_node}); - } - } - - unset($nil->usages); - } - - $result = array(); - $subjects = array_keys((array)$default_graph); - sort($subjects); - foreach($subjects as $subject) { - $node = $default_graph->{$subject}; - if(property_exists($graph_map, $subject)) { - $node->{'@graph'} = array(); - $graph_object = $graph_map->{$subject}; - $subjects_ = array_keys((array)$graph_object); - sort($subjects_); - foreach($subjects_ as $subject_) { - $node_ = $graph_object->{$subject_}; - // only add full subjects to top-level - if(!self::_isSubjectReference($node_)) { - $node->{'@graph'}[] = $node_; - } - } - } - // only add full subjects to top-level - if(!self::_isSubjectReference($node)) { - $result[] = $node; - } - } - - return $result; - } - - /** - * Processes a local context and returns a new active context. - * - * @param stdClass $active_ctx the current active context. - * @param mixed $local_ctx the local context to process. - * @param assoc $options the context processing options. - * - * @return stdClass the new active context. - */ - protected function _processContext($active_ctx, $local_ctx, $options) { - global $jsonld_cache; - - // normalize local context to an array - if(is_object($local_ctx) && property_exists($local_ctx, '@context') && - is_array($local_ctx->{'@context'})) { - $local_ctx = $local_ctx->{'@context'}; - } - $ctxs = self::arrayify($local_ctx); - - // no contexts in array, clone existing context - if(count($ctxs) === 0) { - return self::_cloneActiveContext($active_ctx); - } - - // process each context in order, update active context - // on each iteration to ensure proper caching - $rval = $active_ctx; - foreach($ctxs as $ctx) { - // reset to initial context - if($ctx === null) { - $rval = $active_ctx = $this->_getInitialContext($options); - continue; - } - - // dereference @context key if present - if(is_object($ctx) && property_exists($ctx, '@context')) { - $ctx = $ctx->{'@context'}; - } - - // context must be an object by now, all URLs retrieved before this call - if(!is_object($ctx)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context must be an object.', - 'jsonld.SyntaxError', 'invalid local context', - array('context' => $ctx)); - } - - // get context from cache if available - if(property_exists($jsonld_cache, 'activeCtx')) { - $cached = $jsonld_cache->activeCtx->get($active_ctx, $ctx); - if($cached) { - $rval = $active_ctx = $cached; - $must_clone = true; - continue; - } - } - - // update active context and clone new one before updating - $active_ctx = $rval; - $rval = self::_cloneActiveContext($rval); - - // define context mappings for keys in local context - $defined = new stdClass(); - - // handle @base - if(property_exists($ctx, '@base')) { - $base = $ctx->{'@base'}; - if($base === null) { - $base = null; - } else if(!is_string($base)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; the value of "@base" in a ' . - '@context must be a string or null.', - 'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx)); - } else if($base !== '' && !self::_isAbsoluteIri($base)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; the value of "@base" in a ' . - '@context must be an absolute IRI or the empty string.', - 'jsonld.SyntaxError', 'invalid base IRI', array('context' => $ctx)); - } - if($base !== null) { - $base = jsonld_parse_url($base); - } - $rval->{'@base'} = $base; - $defined->{'@base'} = true; - } - - // handle @vocab - if(property_exists($ctx, '@vocab')) { - $value = $ctx->{'@vocab'}; - if($value === null) { - unset($rval->{'@vocab'}); - } else if(!is_string($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; the value of "@vocab" in a ' . - '@context must be a string or null.', - 'jsonld.SyntaxError', 'invalid vocab mapping', - array('context' => $ctx)); - } else if(!self::_isAbsoluteIri($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; the value of "@vocab" in a ' . - '@context must be an absolute IRI.', - 'jsonld.SyntaxError', 'invalid vocab mapping', - array('context' => $ctx)); - } else { - $rval->{'@vocab'} = $value; - } - $defined->{'@vocab'} = true; - } - - // handle @language - if(property_exists($ctx, '@language')) { - $value = $ctx->{'@language'}; - if($value === null) { - unset($rval->{'@language'}); - } else if(!is_string($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; the value of "@language" in a ' . - '@context must be a string or null.', - 'jsonld.SyntaxError', 'invalid default language', - array('context' => $ctx)); - } else { - $rval->{'@language'} = strtolower($value); - } - $defined->{'@language'} = true; - } - - // process all other keys - foreach($ctx as $k => $v) { - $this->_createTermDefinition($rval, $ctx, $k, $defined); - } - - // cache result - if(property_exists($jsonld_cache, 'activeCtx')) { - $jsonld_cache->activeCtx->set($active_ctx, $ctx, $rval); - } - } - - return $rval; - } - - /** - * Expands a language map. - * - * @param stdClass $language_map the language map to expand. - * - * @return array the expanded language map. - */ - protected function _expandLanguageMap($language_map) { - $rval = array(); - $keys = array_keys((array)$language_map); - sort($keys); - foreach($keys as $key) { - $values = $language_map->{$key}; - $values = self::arrayify($values); - foreach($values as $item) { - if($item === null) { - continue; - } - if(!is_string($item)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; language map values must be strings.', - 'jsonld.SyntaxError', 'invalid language map value', - array('languageMap', $language_map)); - } - $rval[] = (object)array( - '@value' => $item, - '@language' => strtolower($key)); - } - } - return $rval; - } - - /** - * Labels the blank nodes in the given value using the given UniqueNamer. - * - * @param UniqueNamer $namer the UniqueNamer to use. - * @param mixed $element the element with blank nodes to rename. - * - * @return mixed the element. - */ - public function _labelBlankNodes($namer, $element) { - if(is_array($element)) { - $length = count($element); - for($i = 0; $i < $length; ++$i) { - $element[$i] = $this->_labelBlankNodes($namer, $element[$i]); - } - } else if(self::_isList($element)) { - $element->{'@list'} = $this->_labelBlankNodes( - $namer, $element->{'@list'}); - } else if(is_object($element)) { - // rename blank node - if(self::_isBlankNode($element)) { - $name = null; - if(property_exists($element, '@id')) { - $name = $element->{'@id'}; - } - $element->{'@id'} = $namer->getName($name); - } - - // recursively apply to all keys - $keys = array_keys((array)$element); - sort($keys); - foreach($keys as $key) { - if($key !== '@id') { - $element->{$key} = $this->_labelBlankNodes($namer, $element->{$key}); - } - } - } - - return $element; - } - - /** - * Expands the given value by using the coercion and keyword rules in the - * given context. - * - * @param stdClass $active_ctx the active context to use. - * @param string $active_property the property the value is associated with. - * @param mixed $value the value to expand. - * - * @return mixed the expanded value. - */ - protected function _expandValue($active_ctx, $active_property, $value) { - // nothing to expand - if($value === null) { - return null; - } - - // special-case expand @id and @type (skips '@id' expansion) - $expanded_property = $this->_expandIri( - $active_ctx, $active_property, array('vocab' => true)); - if($expanded_property === '@id') { - return $this->_expandIri($active_ctx, $value, array('base' => true)); - } else if($expanded_property === '@type') { - return $this->_expandIri( - $active_ctx, $value, array('vocab' => true, 'base' => true)); - } - - // get type definition from context - $type = self::getContextValue($active_ctx, $active_property, '@type'); - - // do @id expansion (automatic for @graph) - if($type === '@id' || ($expanded_property === '@graph' && - is_string($value))) { - return (object)array('@id' => $this->_expandIri( - $active_ctx, $value, array('base' => true))); - } - // do @id expansion w/vocab - if($type === '@vocab') { - return (object)array('@id' => $this->_expandIri( - $active_ctx, $value, array('vocab' => true, 'base' => true))); - } - - // do not expand keyword values - if(self::_isKeyword($expanded_property)) { - return $value; - } - - $rval = new stdClass(); - - // other type - if($type !== null) { - $rval->{'@type'} = $type; - } else if(is_string($value)) { - // check for language tagging for strings - $language = self::getContextValue( - $active_ctx, $active_property, '@language'); - if($language !== null) { - $rval->{'@language'} = $language; - } - } - $rval->{'@value'} = $value; - - return $rval; - } - - /** - * Creates an array of RDF triples for the given graph. - * - * @param stdClass $graph the graph to create RDF triples for. - * @param UniqueNamer $namer for assigning bnode names. - * @param assoc $options the RDF serialization options. - * - * @return array the array of RDF triples for the given graph. - */ - protected function _graphToRDF($graph, $namer, $options) { - $rval = array(); - - $ids = array_keys((array)$graph); - sort($ids); - foreach($ids as $id) { - $node = $graph->{$id}; - if($id === '"') { - $id = ''; - } - $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)) { - continue; - } - - foreach($items as $item) { - // skip relative IRI subjects and predicates - if(!(self::_isAbsoluteIri($id) && self::_isAbsoluteIri($property))) { - continue; - } - - // RDF subject - $subject = new stdClass(); - $subject->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; - $subject->value = $id; - - // RDF predicate - $predicate = new stdClass(); - $predicate->type = (strpos($property, '_:') === 0 ? - 'blank node' : 'IRI'); - $predicate->value = $property; - - // skip bnode predicates unless producing generalized RDF - if($predicate->type === 'blank node' && - !$options['produceGeneralizedRdf']) { - continue; - } - - if(self::_isList($item)) { - // convert @list to triples - $this->_listToRDF( - $item->{'@list'}, $namer, $subject, $predicate, $rval); - } else { - // convert value or node object to triple - $object = $this->_objectToRDF($item); - // skip null objects (they are relative IRIs) - if($object) { - $rval[] = (object)array( - 'subject' => $subject, - 'predicate' => $predicate, - 'object' => $object); - } - } - } - } - } - - return $rval; - } - - /** - * Converts a @list value into linked list of blank node RDF triples - * (an RDF collection). - * - * @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. - * @param stdClass $predicate the predicate for the head of the list. - * @param &array $triples the array of triples to append to. - */ - protected function _listToRDF( - $list, $namer, $subject, $predicate, &$triples) { - $first = (object)array('type' => 'IRI', 'value' => self::RDF_FIRST); - $rest = (object)array('type' => 'IRI', 'value' => self::RDF_REST); - $nil = (object)array('type' => 'IRI', 'value' => self::RDF_NIL); - - foreach($list as $item) { - $blank_node = (object)array( - 'type' => 'blank node', 'value' => $namer->getName()); - $triples[] = (object)array( - 'subject' => $subject, - 'predicate' => $predicate, - 'object' => $blank_node); - - $subject = $blank_node; - $predicate = $first; - $object = $this->_objectToRDF($item); - // skip null objects (they are relative IRIs) - if($object) { - $triples[] = (object)array( - 'subject' => $subject, - 'predicate' => $predicate, - 'object' => $object); - } - - $predicate = $rest; - } - - $triples[] = (object)array( - 'subject' => $subject, 'predicate' => $predicate, 'object' => $nil); - } - - /** - * Converts a JSON-LD value object to an RDF literal or a JSON-LD string or - * node object to an RDF resource. - * - * @param mixed $item the JSON-LD value or node object. - * - * @return stdClass the RDF literal or RDF resource. - */ - protected function _objectToRDF($item) { - $object = new stdClass(); - - if(self::_isValue($item)) { - $object->type = 'literal'; - $value = $item->{'@value'}; - $datatype = property_exists($item, '@type') ? $item->{'@type'} : null; - - // convert to XSD datatypes as appropriate - if(is_bool($value)) { - $object->value = ($value ? 'true' : 'false'); - $object->datatype = $datatype ? $datatype : self::XSD_BOOLEAN; - } else if(is_double($value) || $datatype == self::XSD_DOUBLE) { - // canonical double representation - $object->value = preg_replace( - '/(\d)0*E\+?/', '$1E', sprintf('%1.15E', $value)); - $object->datatype = $datatype ? $datatype : self::XSD_DOUBLE; - } else if(is_integer($value)) { - $object->value = strval($value); - $object->datatype = $datatype ? $datatype : self::XSD_INTEGER; - } else if(property_exists($item, '@language')) { - $object->value = $value; - $object->datatype = $datatype ? $datatype : self::RDF_LANGSTRING; - $object->language = $item->{'@language'}; - } else { - $object->value = $value; - $object->datatype = $datatype ? $datatype : self::XSD_STRING; - } - } else { - // convert string/node object to RDF - $id = is_object($item) ? $item->{'@id'} : $item; - $object->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; - $object->value = $id; - } - - // skip relative IRIs - if($object->type === 'IRI' && !self::_isAbsoluteIri($object->value)) { - return null; - } - - return $object; - } - - /** - * Converts an RDF triple object to a JSON-LD object. - * - * @param stdClass $o the RDF triple object to convert. - * @param bool $use_native_types true to output native types, false not to. - * - * @return stdClass the JSON-LD object. - */ - protected function _RDFToObject($o, $use_native_types) { - // convert IRI/blank node object to JSON-LD - if($o->type === 'IRI' || $o->type === 'blank node') { - return (object)array('@id' => $o->value); - } - - // convert literal object to JSON-LD - $rval = (object)array('@value' => $o->value); - - if(property_exists($o, 'language')) { - // add language - $rval->{'@language'} = $o->language; - } else { - // add datatype - $type = $o->datatype; - // use native types for certain xsd types - if($use_native_types) { - if($type === self::XSD_BOOLEAN) { - if($rval->{'@value'} === 'true') { - $rval->{'@value'} = true; - } else if($rval->{'@value'} === 'false') { - $rval->{'@value'} = false; - } - } else if(is_numeric($rval->{'@value'})) { - if($type === self::XSD_INTEGER) { - $i = intval($rval->{'@value'}); - if(strval($i) === $rval->{'@value'}) { - $rval->{'@value'} = $i; - } - } else if($type === self::XSD_DOUBLE) { - $rval->{'@value'} = doubleval($rval->{'@value'}); - } - } - // do not add native type - if(!in_array($type, array( - self::XSD_BOOLEAN, self::XSD_INTEGER, self::XSD_DOUBLE, - self::XSD_STRING))) { - $rval->{'@type'} = $type; - } - } else if($type !== self::XSD_STRING) { - $rval->{'@type'} = $type; - } - } - - return $rval; - } - - /** - * Recursively flattens the subjects in the given JSON-LD expanded input - * into a node map. - * - * @param mixed $input the JSON-LD expanded input. - * @param stdClass $graphs a map of graph name to subject map. - * @param string $graph the name of the current graph. - * @param UniqueNamer $namer the blank node namer. - * @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) { - // recurse through array - if(is_array($input)) { - foreach($input as $e) { - $this->_createNodeMap($e, $graphs, $graph, $namer, null, $list); - } - return; - } - - // add non-object to list - if(!is_object($input)) { - if($list !== null) { - $list[] = $input; - } - return; - } - - // add values to list - if(self::_isValue($input)) { - if(property_exists($input, '@type')) { - $type = $input->{'@type'}; - // rename @type blank node - if(strpos($type, '_:') === 0) { - $type = $input->{'@type'} = $namer->getName($type); - } - } - if($list !== null) { - $list[] = $input; - } - return; - } - - // Note: At this point, input must be a subject. - - // spec requires @type to be named first, so assign names early - if(property_exists($input, '@type')) { - foreach($input->{'@type'} as $type) { - if(strpos($type, '_:') === 0) { - $namer->getName($type); - } - } - } - - // get name for subject - if($name === null) { - if(property_exists($input, '@id')) { - $name = $input->{'@id'}; - } - if(self::_isBlankNode($input)) { - $name = $namer->getName($name); - } - } - - // add subject reference to list - if($list !== null) { - $list[] = (object)array('@id' => $name); - } - - // create new subject or merge into existing one - if(!property_exists($graphs, $graph)) { - $graphs->{$graph} = new stdClass(); - } - $subjects = $graphs->{$graph}; - if(!property_exists($subjects, $name)) { - if($name === '') { - $subjects->{'"'} = new stdClass(); - } else { - $subjects->{$name} = new stdClass(); - } - } - if($name === '') { - $subject = $subjects->{'"'}; - } else { - $subject = $subjects->{$name}; - } - $subject->{'@id'} = $name; - $properties = array_keys((array)$input); - sort($properties); - foreach($properties as $property) { - // skip @id - if($property === '@id') { - continue; - } - - // handle reverse properties - if($property === '@reverse') { - $referenced_node = (object)array('@id' => $name); - $reverse_map = $input->{'@reverse'}; - foreach($reverse_map as $reverse_property => $items) { - foreach($items as $item) { - $item_name = null; - if(property_exists($item, '@id')) { - $item_name = $item->{'@id'}; - } - if(self::_isBlankNode($item)) { - $item_name = $namer->getName($item_name); - } - $this->_createNodeMap($item, $graphs, $graph, $namer, $item_name); - if($item_name === '') { - $item_name = '"'; - } - self::addValue( - $subjects->{$item_name}, $reverse_property, $referenced_node, - array('propertyIsArray' => true, 'allowDuplicate' => false)); - } - } - continue; - } - - // recurse into graph - if($property === '@graph') { - // add graph subjects map entry - if(!property_exists($graphs, $name)) { - // FIXME: temporary hack to avoid empty property bug - if(!$name) { - $name = '"'; - } - $graphs->{$name} = new stdClass(); - } - $g = ($graph === '@merged') ? $graph : $name; - $this->_createNodeMap( - $input->{$property}, $graphs, $g, $namer, null, null); - continue; - } - - // copy non-@type keywords - if($property !== '@type' && self::_isKeyword($property)) { - if($property === '@index' && property_exists($subject, '@index') && - ($input->{'@index'} !== $subject->{'@index'} || - $input->{'@index'}->{'@id'} !== $subject->{'@index'}->{'@id'})) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; conflicting @index property detected.', - 'jsonld.SyntaxError', 'conflicting indexes', - array('subject' => $subject)); - } - $subject->{$property} = $input->{$property}; - continue; - } - - // iterate over objects - $objects = $input->{$property}; - - // if property is a bnode, assign it a new id - if(strpos($property, '_:') === 0) { - $property = $namer->getName($property); - } - - // ensure property is added for empty arrays - if(count($objects) === 0) { - self::addValue( - $subject, $property, array(), array('propertyIsArray' => true)); - continue; - } - foreach($objects as $o) { - if($property === '@type') { - // rename @type blank nodes - $o = (strpos($o, '_:') === 0) ? $namer->getName($o) : $o; - } - - // handle embedded subject or subject reference - if(self::_isSubject($o) || self::_isSubjectReference($o)) { - // rename blank node @id - $id = property_exists($o, '@id') ? $o->{'@id'} : null; - if(self::_isBlankNode($o)) { - $id = $namer->getName($id); - } - - // add reference and recurse - self::addValue( - $subject, $property, (object)array('@id' => $id), - array('propertyIsArray' => true, 'allowDuplicate' => false)); - $this->_createNodeMap($o, $graphs, $graph, $namer, $id, null); - } else if(self::_isList($o)) { - // handle @list - $_list = new ArrayObject(); - $this->_createNodeMap( - $o->{'@list'}, $graphs, $graph, $namer, $name, $_list); - $o = (object)array('@list' => (array)$_list); - self::addValue( - $subject, $property, $o, - array('propertyIsArray' => true, 'allowDuplicate' => false)); - } else { - // handle @value - $this->_createNodeMap($o, $graphs, $graph, $namer, $name, null); - self::addValue( - $subject, $property, $o, - array('propertyIsArray' => true, 'allowDuplicate' => false)); - } - } - } - } - - /** - * Frames subjects according to the given frame. - * - * @param stdClass $state the current framing state. - * @param array $subjects the subjects to filter. - * @param array $frame the frame. - * @param mixed $parent the parent subject or top-level array. - * @param mixed $property the parent property, initialized to null. - */ - protected function _matchFrame( - $state, $subjects, $frame, $parent, $property) { - // validate the frame - $this->_validateFrame($frame); - $frame = $frame[0]; - - // get flags for current frame - $options = $state->options; - $flags = array( - 'embed' => $this->_getFrameFlag($frame, $options, 'embed'), - 'explicit' => $this->_getFrameFlag($frame, $options, 'explicit'), - 'requireAll' => $this->_getFrameFlag($frame, $options, 'requireAll')); - - // filter out subjects that match the frame - $matches = $this->_filterSubjects($state, $subjects, $frame, $flags); - - // add matches to output - foreach($matches as $id => $subject) { - if($flags['embed'] === '@link' && property_exists($state->link, $id)) { - // TODO: may want to also match an existing linked subject against - // the current frame ... so different frames could produce different - // subjects that are only shared in-memory when the frames are the same - - // add existing linked subject - $this->_addFrameOutput($parent, $property, $state->link->{$id}); - 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, - which only occurs at the top-level. */ - if($property === null) { - $state->uniqueEmbeds = new stdClass(); - } - - // 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 - // embed, the subject cannot be embedded, just add the reference; - // note that a circular reference won't occur when the embed flag is - // `@link` as the above check will short-circuit before reaching this point - if($flags['embed'] === '@never' || - $this->_createsCircularReference($subject, $state->subjectStack)) { - $this->_addFrameOutput($parent, $property, $output); - continue; - } - - // if only the last match should be embedded - if($flags['embed'] === '@last') { - // remove any existing embed - if(property_exists($state->uniqueEmbeds, $id)) { - $this->_removeEmbed($state, $id); - } - $state->uniqueEmbeds->{$id} = array( - 'parent' => $parent, 'property' => $property); - } - - // push matching subject onto stack to enable circular embed checks - $state->subjectStack[] = $subject; - - // iterate over subject properties - $props = array_keys((array)$subject); - sort($props); - foreach($props as $prop) { - // copy keywords to output - if(self::_isKeyword($prop)) { - $output->{$prop} = self::copy($subject->{$prop}); - continue; - } - - // explicit is on and property isn't in the frame, skip processing - if($flags['explicit'] && !property_exists($frame, $prop)) { - continue; - } - - // add objects - $objects = $subject->{$prop}; - foreach($objects as $o) { - // recurse into list - if(self::_isList($o)) { - // add empty list - $list = (object)array('@list' => array()); - $this->_addFrameOutput($output, $prop, $list); - - // add list objects - $src = $o->{'@list'}; - foreach($src as $o) { - if(self::_isSubjectReference($o)) { - // recurse into subject reference - $subframe = (property_exists($frame, $prop) ? - $frame->{$prop}[0]->{'@list'} : - $this->_createImplicitFrame($flags)); - $this->_matchFrame( - $state, array($o->{'@id'}), $subframe, $list, '@list'); - } else { - // include other values automatically - $this->_addFrameOutput($list, '@list', self::copy($o)); - } - } - continue; - } - - if(self::_isSubjectReference($o)) { - // recurse into subject reference - $subframe = (property_exists($frame, $prop) ? - $frame->{$prop} : $this->_createImplicitFrame($flags)); - $this->_matchFrame( - $state, array($o->{'@id'}), $subframe, $output, $prop); - } else { - // include other values automatically - $this->_addFrameOutput($output, $prop, self::copy($o)); - } - } - } - - // handle defaults - $props = array_keys((array)$frame); - sort($props); - foreach($props as $prop) { - // skip keywords - if(self::_isKeyword($prop)) { - continue; - } - - // if omit default is off, then include default values for properties - // that appear in the next frame but are not in the matching subject - $next = $frame->{$prop}[0]; - $omit_default_on = $this->_getFrameFlag( - $next, $options, 'omitDefault'); - if(!$omit_default_on && !property_exists($output, $prop)) { - $preserve = '@null'; - if(property_exists($next, '@default')) { - $preserve = self::copy($next->{'@default'}); - } - $preserve = self::arrayify($preserve); - $output->{$prop} = array((object)array('@preserve' => $preserve)); - } - } - - // add output to parent - $this->_addFrameOutput($parent, $property, $output); - - // pop matching subject from circular ref-checking stack - array_pop($state->subjectStack); - } - } - - /** - * 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 - * parent frame used. - * - * @param assoc flags the current framing flags. - * - * @return array the implicit frame. - */ - function _createImplicitFrame($flags) { - $frame = new stdClass(); - foreach($flags as $key => $value) { - $frame->{'@' . $key} = array($flags[$key]); - } - return array($frame); - } - - /** - * Checks the current subject stack to see if embedding the given subject - * would cause a circular reference. - * - * @param stdClass subject_to_embed the subject to embed. - * @param assoc subject_stack the current stack of subjects. - * - * @return bool true if a circular reference would be created, false if not. - */ - function _createsCircularReference($subject_to_embed, $subject_stack) { - for($i = count($subject_stack) - 1; $i >= 0; --$i) { - if($subject_stack[$i]->{'@id'} === $subject_to_embed->{'@id'}) { - return true; - } - } - return false; - } - - /** - * Gets the frame flag value for the given flag name. - * - * @param stdClass $frame the frame. - * @param stdClass $options the framing options. - * @param string $name the flag name. - * - * @return mixed $the flag value. - */ - protected function _getFrameFlag($frame, $options, $name) { - $flag = "@$name"; - $rval = (property_exists($frame, $flag) ? - $frame->{$flag}[0] : $options[$name]); - if($name === 'embed') { - // default is "@last" - // backwards-compatibility support for "embed" maps: - // true => "@last" - // false => "@never" - if($rval === true) { - $rval = '@last'; - } else if($rval === false) { - $rval = '@never'; - } else if($rval !== '@always' && $rval !== '@never' && - $rval !== '@link') { - $rval = '@last'; - } - } - return $rval; - } - - /** - * Validates a JSON-LD frame, throwing an exception if the frame is invalid. - * - * @param array $frame the frame to validate. - */ - protected function _validateFrame($frame) { - if(!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.', - 'jsonld.SyntaxError', null, array('frame' => $frame)); - } - } - - /** - * Returns a map of all of the subjects that match a parsed frame. - * - * @param stdClass $state the current framing state. - * @param array $subjects the set of subjects to filter. - * @param stdClass $frame the parsed frame. - * @param assoc $flags the frame flags. - * - * @return stdClass all of the matched subjects. - */ - protected function _filterSubjects($state, $subjects, $frame, $flags) { - $rval = new stdClass(); - sort($subjects); - foreach($subjects as $id) { - $subject = $state->subjects->{$id}; - if($this->_filterSubject($subject, $frame, $flags)) { - $rval->{$id} = $subject; - } - } - return $rval; - } - - /** - * Returns true if the given subject matches the given frame. - * - * @param stdClass $subject the subject to check. - * @param stdClass $frame the frame to check. - * @param assoc $flags the frame flags. - * - * @return bool true if the subject matches, false if not. - */ - protected function _filterSubject($subject, $frame, $flags) { - // check @type (object value means 'any' type, fall through to ducktyping) - if(property_exists($frame, '@type') && - !(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) { - $types = $frame->{'@type'}; - foreach($types as $type) { - // any matching @type is a match - if(self::hasValue($subject, '@type', $type)) { - return true; - } - } - return false; - } - - // check ducktype - $wildcard = true; - $matches_some = false; - foreach($frame as $k => $v) { - if(self::_isKeyword($k)) { - // skip non-@id and non-@type - if($k !== '@id' && $k !== '@type') { - continue; - } - $wildcard = false; - - // check @id for a specific @id value - if($k === '@id' && is_string($v)) { - if(!property_exists($subject, $k) || $subject->{$k} !== $v) { - return false; - } - $matches_some = true; - continue; - } - } - - $wildcard = false; - - if(property_exists($subject, $k)) { - // $v === [] means do not match if property is present - if(is_array($v) && count($v) === 0) { - return false; - } - $matches_some = true; - continue; - } - - // all properties must match to be a duck unless a @default is specified - $has_default = (is_array($v) && count($v) === 1 && is_object($v[0]) && - property_exists($v[0], '@default')); - if($flags['requireAll'] && !$has_default) { - return false; - } - } - - // return true if wildcard or subject matches some properties - return $wildcard || $matches_some; - } - - /** - * Removes an existing embed. - * - * @param stdClass $state the current framing state. - * @param string $id the @id of the embed to remove. - */ - protected function _removeEmbed($state, $id) { - // get existing embed - $embeds = $state->uniqueEmbeds; - $embed = $embeds->{$id}; - $property = $embed['property']; - - // create reference to replace embed - $subject = (object)array('@id' => $id); - - // remove existing embed - if(is_array($embed->parent)) { - // replace subject with reference - foreach($embed->parent as $i => $parent) { - if(self::compareValues($parent, $subject)) { - $embed->parent[$i] = $subject; - break; - } - } - } else { - // replace subject with reference - $use_array = is_array($embed->parent->{$property}); - self::removeValue($embed->parent, $property, $subject, - array('propertyIsArray' => $use_array)); - self::addValue($embed->parent, $property, $subject, - array('propertyIsArray' => $use_array)); - } - - // recursively remove dependent dangling embeds - $removeDependents = function($id) { - // get embed keys as a separate array to enable deleting keys in map - $ids = array_keys((array)$embeds); - foreach($ids as $next) { - if(property_exists($embeds, $next) && - is_object($embeds->{$next}->parent) && - $embeds->{$next}->parent->{'@id'} === $id) { - unset($embeds->{$next}); - $removeDependents($next); - } - } - }; - $removeDependents($id); - } - - /** - * Adds framing output to the given parent. - * - * @param mixed $parent the parent to add to. - * @param string $property the parent property. - * @param mixed $output the output to add. - */ - protected function _addFrameOutput($parent, $property, $output) { - if(is_object($parent) && !($parent instanceof ArrayObject)) { - self::addValue( - $parent, $property, $output, array('propertyIsArray' => true)); - } else { - $parent[] = $output; - } - } - - /** - * Removes the @preserve keywords as the last step of the framing algorithm. - * - * @param stdClass $ctx the active context used to compact the input. - * @param mixed $input the framed, compacted output. - * @param assoc $options the compaction options used. - * - * @return mixed the resulting output. - */ - protected function _removePreserve($ctx, $input, $options) { - // recurse through arrays - if(is_array($input)) { - $output = array(); - foreach($input as $e) { - $result = $this->_removePreserve($ctx, $e, $options); - // drop nulls from arrays - if($result !== null) { - $output[] = $result; - } - } - $input = $output; - } else if(is_object($input)) { - // remove @preserve - if(property_exists($input, '@preserve')) { - if($input->{'@preserve'} === '@null') { - return null; - } - return $input->{'@preserve'}; - } - - // skip @values - if(self::_isValue($input)) { - return $input; - } - - // recurse through @lists - if(self::_isList($input)) { - $input->{'@list'} = $this->_removePreserve( - $ctx, $input->{'@list'}, $options); - return $input; - } - - // handle in-memory linked nodes - $id_alias = $this->_compactIri($ctx, '@id'); - if(property_exists($input, $id_alias)) { - $id = $input->{$id_alias}; - if(isset($options['link'][$id])) { - $idx = array_search($input, $options['link'][$id]); - if($idx === false) { - // prevent circular visitation - $options['link'][$id][] = $input; - } else { - // already visited - return $options['link'][$id][$idx]; - } - } else { - // prevent circular visitation - $options['link'][$id] = array($input); - } - } - - // recurse through properties - foreach($input as $prop => $v) { - $result = $this->_removePreserve($ctx, $v, $options); - $container = self::getContextValue($ctx, $prop, '@container'); - if($options['compactArrays'] && - is_array($result) && count($result) === 1 && - $container !== '@set' && $container !== '@list') { - $result = $result[0]; - } - $input->{$prop} = $result; - } - } - return $input; - } - - /** - * Compares two RDF triples for equality. - * - * @param stdClass $t1 the first triple. - * @param stdClass $t2 the second triple. - * - * @return true if the triples are the same, false if not. - */ - protected static function _compareRDFTriples($t1, $t2) { - foreach(array('subject', 'predicate', 'object') as $attr) { - if($t1->{$attr}->type !== $t2->{$attr}->type || - $t1->{$attr}->value !== $t2->{$attr}->value) { - return false; - } - } - if(property_exists($t1->object, 'language') !== - property_exists($t1->object, 'language')) { - return false; - } - if(property_exists($t1->object, 'language') && - $t1->object->language !== $t2->object->language) { - return false; - } - if(property_exists($t1->object, 'datatype') && - $t1->object->datatype !== $t2->object->datatype) { - return false; - } - return true; - } - - /** - * Hashes all of the quads about a blank node. - * - * @param string $id the ID of the bnode to hash quads for. - * @param stdClass $bnodes the mapping of bnodes to quads. - * @param UniqueNamer $namer the canonical bnode namer. - * - * @return string the new hash. - */ - protected function _hashQuads($id, $bnodes, $namer) { - // return cached hash - if(property_exists($bnodes->{$id}, 'hash')) { - return $bnodes->{$id}->hash; - } - - // serialize all of bnode's quads - $quads = $bnodes->{$id}->quads; - $nquads = array(); - foreach($quads as $quad) { - $nquads[] = $this->toNQuad($quad, property_exists($quad, 'name') ? - $quad->name->value : null, $id); - } - - // sort serialized quads - sort($nquads); - - // cache and return hashed quads - $hash = $bnodes->{$id}->hash = sha1(implode($nquads)); - return $hash; - } - - /** - * 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 - * lexicographically-least 'path' serializations. - * - * @param string $id the ID of the bnode to hash paths for. - * @param stdClass $bnodes the map of bnode quads. - * @param UniqueNamer $namer the canonical bnode namer. - * @param UniqueNamer $path_namer the namer used to assign names to adjacent - * bnodes. - * - * @return stdClass the hash and path namer used. - */ - protected function _hashPaths($id, $bnodes, $namer, $path_namer) { - // create SHA-1 digest - $md = hash_init('sha1'); - - // group adjacent bnodes by hash, keep properties and references separate - $groups = new stdClass(); - $quads = $bnodes->{$id}->quads; - foreach($quads as $quad) { - // get adjacent bnode - $bnode = $this->_getAdjacentBlankNodeName($quad->subject, $id); - if($bnode !== null) { - // normal property - $direction = 'p'; - } else { - $bnode = $this->_getAdjacentBlankNodeName($quad->object, $id); - if($bnode !== null) { - // reverse property - $direction = 'r'; - } - } - if($bnode !== null) { - // get bnode name (try canonical, path, then hash) - if($namer->isNamed($bnode)) { - $name = $namer->getName($bnode); - } else if($path_namer->isNamed($bnode)) { - $name = $path_namer->getName($bnode); - } else { - $name = $this->_hashQuads($bnode, $bnodes, $namer); - } - - // hash direction, property, and bnode name/hash - $group_md = hash_init('sha1'); - hash_update($group_md, $direction); - hash_update($group_md, $quad->predicate->value); - hash_update($group_md, $name); - $group_hash = hash_final($group_md); - - // add bnode to hash group - if(property_exists($groups, $group_hash)) { - $groups->{$group_hash}[] = $bnode; - } else { - $groups->{$group_hash} = array($bnode); - } - } - } - - // iterate over groups in sorted hash order - $group_hashes = array_keys((array)$groups); - sort($group_hashes); - foreach($group_hashes as $group_hash) { - // digest group hash - hash_update($md, $group_hash); - - // choose a path and namer from the permutations - $chosen_path = null; - $chosen_namer = null; - $permutator = new Permutator($groups->{$group_hash}); - while($permutator->hasNext()) { - $permutation = $permutator->next(); - $path_namer_copy = clone $path_namer; - - // build adjacent path - $path = ''; - $skipped = false; - $recurse = array(); - foreach($permutation as $bnode) { - // use canonical name if available - if($namer->isNamed($bnode)) { - $path .= $namer->getName($bnode); - } else { - // recurse if bnode isn't named in the path yet - if(!$path_namer_copy->isNamed($bnode)) { - $recurse[] = $bnode; - } - $path .= $path_namer_copy->getName($bnode); - } - - // skip permutation if path is already >= chosen path - if($chosen_path !== null && strlen($path) >= strlen($chosen_path) && - $path > $chosen_path) { - $skipped = true; - break; - } - } - - // recurse - if(!$skipped) { - foreach($recurse as $bnode) { - $result = $this->_hashPaths( - $bnode, $bnodes, $namer, $path_namer_copy); - $path .= $path_namer_copy->getName($bnode); - $path .= "<{$result->hash}>"; - $path_namer_copy = $result->pathNamer; - - // skip permutation if path is already >= chosen path - if($chosen_path !== null && - strlen($path) >= strlen($chosen_path) && $path > $chosen_path) { - $skipped = true; - break; - } - } - } - - if(!$skipped && ($chosen_path === null || $path < $chosen_path)) { - $chosen_path = $path; - $chosen_namer = $path_namer_copy; - } - } - - // digest chosen path and update namer - hash_update($md, $chosen_path); - $path_namer = $chosen_namer; - } - - // return SHA-1 hash and path namer - return (object)array( - 'hash' => hash_final($md), 'pathNamer' => $path_namer); - } - - /** - * 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 - * 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. - * - * @return mixed the adjacent blank node name or null if none was found. - */ - protected function _getAdjacentBlankNodeName($node, $id) { - if($node->type === 'blank node' && $node->value !== $id) { - return $node->value; - } - return null; - } - - /** - * Compares two strings first based on length and then lexicographically. - * - * @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. - */ - protected function _compareShortestLeast($a, $b) { - $len_a = strlen($a); - $len_b = strlen($b); - if($len_a < $len_b) { - return -1; - } - if($len_b < $len_a) { - return 1; - } - if($a === $b) { - return 0; - } - return ($a < $b) ? -1 : 1; - } - - /** - * Picks the preferred compaction term from the given inverse context entry. - * - * @param active_ctx the active context. - * @param iri the IRI to pick the term for. - * @param value the value to pick the term for. - * @param containers the preferred containers. - * @param type_or_language either '@type' or '@language'. - * @param type_or_language_value the preferred value for '@type' or - * '@language'. - * - * @return mixed the preferred term. - */ - protected function _selectTerm( - $active_ctx, $iri, $value, $containers, - $type_or_language, $type_or_language_value) { - if($type_or_language_value === null) { - $type_or_language_value = '@null'; - } - - // options for the value of @type or @language - $prefs = array(); - - // determine prefs for @id based on whether or not value compacts to a term - if(($type_or_language_value === '@id' || - $type_or_language_value === '@reverse') && - self::_isSubjectReference($value)) { - // prefer @reverse first - if($type_or_language_value === '@reverse') { - $prefs[] = '@reverse'; - } - // try to compact value to a term - $term = $this->_compactIri( - $active_ctx, $value->{'@id'}, null, array('vocab' => true)); - if(property_exists($active_ctx->mappings, $term) && - $active_ctx->mappings->{$term} && - $active_ctx->mappings->{$term}->{'@id'} === $value->{'@id'}) { - // prefer @vocab - array_push($prefs, '@vocab', '@id'); - } else { - // prefer @id - array_push($prefs, '@id', '@vocab'); - } - } else { - $prefs[] = $type_or_language_value; - } - $prefs[] = '@none'; - - $container_map = $active_ctx->inverse->{$iri}; - foreach($containers as $container) { - // if container not available in the map, continue - if(!property_exists($container_map, $container)) { - continue; - } - - $type_or_language_value_map = - $container_map->{$container}->{$type_or_language}; - foreach($prefs as $pref) { - // if type/language option not available in the map, continue - if(!property_exists($type_or_language_value_map, $pref)) { - continue; - } - - // select term - return $type_or_language_value_map->{$pref}; - } - } - return null; - } - - /** - * 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. - * - * @param stdClass $active_ctx the active context to use. - * @param string $iri the IRI to compact. - * @param mixed $value the value to check or null. - * @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 - * if not. - * - * @return string the compacted term, prefix, keyword alias, or original IRI. - */ - protected function _compactIri( - $active_ctx, $iri, $value=null, $relative_to=array(), $reverse=false) { - // can't compact null - if($iri === null) { - return $iri; - } - - $inverse_ctx = $this->_getInverseContext($active_ctx); - - if(self::_isKeyword($iri)) { - // a keyword can only be compacted to simple alias - if(property_exists($inverse_ctx, $iri)) { - return $inverse_ctx->$iri->{'@none'}->{'@type'}->{'@none'}; - } - return $iri; - } - - if(!isset($relative_to['vocab'])) { - $relative_to['vocab'] = false; - } - - // use inverse context to pick a term if iri is relative to vocab - if($relative_to['vocab'] && property_exists($inverse_ctx, $iri)) { - $default_language = '@none'; - if(property_exists($active_ctx, '@language')) { - $default_language = $active_ctx->{'@language'}; - } - - // prefer @index if available in value - $containers = array(); - if(is_object($value) && property_exists($value, '@index')) { - $containers[] = '@index'; - } - - // defaults for term selection based on type/language - $type_or_language = '@language'; - $type_or_language_value = '@null'; - - if($reverse) { - $type_or_language = '@type'; - $type_or_language_value = '@reverse'; - $containers[] = '@set'; - } 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(!property_exists($value, '@index')) { - $containers[] = '@list'; - } - $list = $value->{'@list'}; - $common_language = (count($list) === 0) ? $default_language : null; - $common_type = null; - foreach($list as $item) { - $item_language = '@none'; - $item_type = '@none'; - if(self::_isValue($item)) { - if(property_exists($item, '@language')) { - $item_language = $item->{'@language'}; - } else if(property_exists($item, '@type')) { - $item_type = $item->{'@type'}; - } else { - // plain literal - $item_language = '@null'; - } - } else { - $item_type = '@id'; - } - if($common_language === null) { - $common_language = $item_language; - } else if($item_language !== $common_language && - self::_isValue($item)) { - $common_language = '@none'; - } - if($common_type === null) { - $common_type = $item_type; - } else if($item_type !== $common_type) { - $common_type = '@none'; - } - // there are different languages and types in the list, so choose - // the most generic term, no need to keep iterating the list - if($common_language === '@none' && $common_type === '@none') { - break; - } - } - if($common_language === null) { - $common_language = '@none'; - } - if($common_type === null) { - $common_type = '@none'; - } - if($common_type !== '@none') { - $type_or_language = '@type'; - $type_or_language_value = $common_type; - } else { - $type_or_language_value = $common_language; - } - } else { - if(self::_isValue($value)) { - if(property_exists($value, '@language') && - !property_exists($value, '@index')) { - $containers[] = '@language'; - $type_or_language_value = $value->{'@language'}; - } else if(property_exists($value, '@type')) { - $type_or_language = '@type'; - $type_or_language_value = $value->{'@type'}; - } - } else { - $type_or_language = '@type'; - $type_or_language_value = '@id'; - } - $containers[] = '@set'; - } - - // do term selection - $containers[] = '@none'; - $term = $this->_selectTerm( - $active_ctx, $iri, $value, - $containers, $type_or_language, $type_or_language_value); - if($term !== null) { - return $term; - } - } - - // no term match, use @vocab if available - if($relative_to['vocab']) { - if(property_exists($active_ctx, '@vocab')) { - // determine if vocab is a prefix of the iri - $vocab = $active_ctx->{'@vocab'}; - if(strpos($iri, $vocab) === 0 && $iri !== $vocab) { - // use suffix as relative iri if it is not a term in the active - // context - $suffix = substr($iri, strlen($vocab)); - if(!property_exists($active_ctx->mappings, $suffix)) { - return $suffix; - } - } - } - } - - // no term or @vocab match, check for possible CURIEs - $choice = null; - $idx = 0; - $partial_matches = array(); - $iri_map = $active_ctx->fast_curie_map; - // check for partial matches of against `iri`, which means look until - // iri.length - 1, not full length - $max_partial_length = strlen($iri) - 1; - for(; $idx < $max_partial_length && isset($iri_map[$iri[$idx]]); ++$idx) { - $iri_map = $iri_map[$iri[$idx]]; - if(isset($iri_map[''])) { - $entry = $iri_map[''][0]; - $entry->iri_length = $idx + 1; - $partial_matches[] = $entry; - } - } - // check partial matches in reverse order to prefer longest ones first - $partial_matches = array_reverse($partial_matches); - foreach($partial_matches as $entry) { - $terms = $entry->terms; - 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 - // the mapping matches the IRI - $curie = $term . ':' . substr($iri, $entry->iri_length); - $is_usable_curie = (!property_exists($active_ctx->mappings, $curie) || - ($value === null && - $active_ctx->mappings->{$curie}->{'@id'} === $iri)); - - // select curie if it is shorter or the same length but - // lexicographically less than the current choice - if($is_usable_curie && ($choice === null || - self::_compareShortestLeast($curie, $choice) < 0)) { - $choice = $curie; - } - } - } - - // return chosen curie - if($choice !== null) { - return $choice; - } - - // compact IRI relative to base - if(!$relative_to['vocab']) { - return jsonld_remove_base($active_ctx->{'@base'}, $iri); - } - - // return IRI as is - return $iri; - } - - /** - * Performs value compaction on an object with '@value' or '@id' as the only - * property. - * - * @param stdClass $active_ctx the active context. - * @param string $active_property the active property that points to the - * value. - * @param mixed $value the value to compact. - * - * @return mixed the compaction result. - */ - protected function _compactValue($active_ctx, $active_property, $value) { - // value is a @value - if(self::_isValue($value)) { - // get context rules - $type = self::getContextValue($active_ctx, $active_property, '@type'); - $language = self::getContextValue( - $active_ctx, $active_property, '@language'); - $container = self::getContextValue( - $active_ctx, $active_property, '@container'); - - // whether or not the value has an @index that must be preserved - $preserve_index = (property_exists($value, '@index') && - $container !== '@index'); - - // if there's no @index to preserve - if(!$preserve_index) { - // matching @type or @language specified in context, compact value - if(self::_hasKeyValue($value, '@type', $type) || - self::_hasKeyValue($value, '@language', $language)) { - return $value->{'@value'}; - } - } - - // return just the value of @value if all are true: - // 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 - $key_count = count(array_keys((array)$value)); - $is_value_only_key = ($key_count === 1 || - ($key_count === 2 && property_exists($value, '@index') && - !$preserve_index)); - $has_default_language = property_exists($active_ctx, '@language'); - $is_value_string = is_string($value->{'@value'}); - $has_null_mapping = ( - property_exists($active_ctx->mappings, $active_property) && - $active_ctx->mappings->{$active_property} !== null && - self::_hasKeyValue( - $active_ctx->mappings->{$active_property}, '@language', null)); - if($is_value_only_key && - (!$has_default_language || !$is_value_string || $has_null_mapping)) { - return $value->{'@value'}; - } - - $rval = new stdClass(); - - // preserve @index - if($preserve_index) { - $rval->{$this->_compactIri($active_ctx, '@index')} = $value->{'@index'}; - } - - // compact @type IRI - if(property_exists($value, '@type')) { - $rval->{$this->_compactIri($active_ctx, '@type')} = $this->_compactIri( - $active_ctx, $value->{'@type'}, null, array('vocab' => true)); - } else if(property_exists($value, '@language')) { - // alias @language - $rval->{$this->_compactIri($active_ctx, '@language')} = - $value->{'@language'}; - } - - // alias @value - $rval->{$this->_compactIri($active_ctx, '@value')} = $value->{'@value'}; - - return $rval; - } - - // value is a subject reference - $expanded_property = $this->_expandIri( - $active_ctx, $active_property, array('vocab' => true)); - $type = self::getContextValue($active_ctx, $active_property, '@type'); - $compacted = $this->_compactIri( - $active_ctx, $value->{'@id'}, null, - array('vocab' => ($type === '@vocab'))); - - // compact to scalar - if($type === '@id' || $type === '@vocab' || - $expanded_property === '@graph') { - return $compacted; - } - - $rval = (object)array( - $this->_compactIri($active_ctx, '@id') => $compacted); - return $rval; - } - - /** - * Creates a term definition during context processing. - * - * @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. - * @param stdClass $defined a map of defining/defined keys to detect cycles - * and prevent double definitions. - */ - protected function _createTermDefinition( - $active_ctx, $local_ctx, $term, $defined) { - if(property_exists($defined, $term)) { - // term already defined - if($defined->{$term}) { - return; - } - // cycle detected - throw new JsonLdException( - 'Cyclical context definition detected.', - 'jsonld.CyclicalContext', 'cyclic IRI mapping', - array('context' => $local_ctx, 'term' => $term)); - } - - // now defining term - $defined->{$term} = false; - - if(self::_isKeyword($term)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; keywords cannot be overridden.', - 'jsonld.SyntaxError', 'keyword redefinition', - array('context' => $local_ctx, 'term' => $term)); - } - - // remove old mapping - if(property_exists($active_ctx->mappings, $term)) { - unset($active_ctx->mappings->{$term}); - } - - // get context term value - $value = $local_ctx->{$term}; - - // clear context entry - if($value === null || (is_object($value) && - self::_hasKeyValue($value, '@id', null))) { - $active_ctx->mappings->{$term} = null; - $defined->{$term} = true; - return; - } - - // convert short-hand value to object w/@id - if(is_string($value)) { - $value = (object)array('@id' => $value); - } - - if(!is_object($value)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context property values must be ' . - 'strings or objects.', 'jsonld.SyntaxError', 'invalid term definition', - array('context' => $local_ctx)); - } - - // create new mapping - $mapping = $active_ctx->mappings->{$term} = new stdClass(); - $mapping->reverse = false; - - if(property_exists($value, '@reverse')) { - if(property_exists($value, '@id')) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; a @reverse term definition must not ' + - 'contain @id.', 'jsonld.SyntaxError', 'invalid reverse property', - array('context' => $local_ctx)); - } - $reverse = $value->{'@reverse'}; - if(!is_string($reverse)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; a @context @reverse value must be a string.', - 'jsonld.SyntaxError', 'invalid IRI mapping', - array('context' => $local_ctx)); - } - - // expand and add @id mapping - $id = $this->_expandIri( - $active_ctx, $reverse, array('vocab' => true, 'base' => false), - $local_ctx, $defined); - if(!self::_isAbsoluteIri($id)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context @reverse value must be ' . - 'an absolute IRI or a blank node identifier.', - 'jsonld.SyntaxError', 'invalid IRI mapping', - array('context' => $local_ctx)); - } - $mapping->{'@id'} = $id; - $mapping->reverse = true; - } else if(property_exists($value, '@id')) { - $id = $value->{'@id'}; - if(!is_string($id)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context @id value must be a string.', - 'jsonld.SyntaxError', 'invalid IRI mapping', - array('context' => $local_ctx)); - } - if($id !== $term) { - // add @id to mapping - $id = $this->_expandIri( - $active_ctx, $id, array('vocab' => true, 'base' => false), - $local_ctx, $defined); - if(!self::_isAbsoluteIri($id) && !self::_isKeyword($id)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context @id value must be an ' . - 'absolute IRI, a blank node identifier, or a keyword.', - 'jsonld.SyntaxError', 'invalid IRI mapping', - array('context' => $local_ctx)); - } - $mapping->{'@id'} = $id; - } - } - - // always compute whether term has a colon as an optimization for - // _compactIri - $colon = strpos($term, ':'); - $mapping->_term_has_colon = ($colon !== false); - - if(!property_exists($mapping, '@id')) { - // see if the term has a prefix - if($mapping->_term_has_colon) { - $prefix = substr($term, 0, $colon); - if(property_exists($local_ctx, $prefix)) { - // define parent prefix - $this->_createTermDefinition( - $active_ctx, $local_ctx, $prefix, $defined); - } - - if(property_exists($active_ctx->mappings, $prefix) && - $active_ctx->mappings->{$prefix}) { - // set @id based on prefix parent - $suffix = substr($term, $colon + 1); - $mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} . - $suffix; - } else { - // term is an absolute IRI - $mapping->{'@id'} = $term; - } - } else { - // non-IRIs *must* define @ids if @vocab is not available - if(!property_exists($active_ctx, '@vocab')) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context terms must define an @id.', - 'jsonld.SyntaxError', 'invalid IRI mapping', - array('context' => $local_ctx, 'term' => $term)); - } - // prepend vocab to term - $mapping->{'@id'} = $active_ctx->{'@vocab'} . $term; - } - } - - // optimization to store length of @id once for _compactIri - $mapping->_id_length = strlen($mapping->{'@id'}); - - // IRI mapping now defined - $defined->{$term} = true; - - if(property_exists($value, '@type')) { - $type = $value->{'@type'}; - if(!is_string($type)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context @type values must be strings.', - 'jsonld.SyntaxError', 'invalid type mapping', - array('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)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; an @context @type value must ' . - 'be an absolute IRI.', 'jsonld.SyntaxError', - 'invalid type mapping', array('context' => $local_ctx)); - } - if(strpos($type, '_:') === 0) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; an @context @type values must ' . - 'be an IRI, not a blank node identifier.', - 'jsonld.SyntaxError', 'invalid type mapping', - array('context' => $local_ctx)); - } - } - - // add @type to mapping - $mapping->{'@type'} = $type; - } - - if(property_exists($value, '@container')) { - $container = $value->{'@container'}; - if($container !== '@list' && $container !== '@set' && - $container !== '@index' && $container !== '@language') { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context @container value must be ' . - 'one of the following: @list, @set, @index, or @language.', - 'jsonld.SyntaxError', 'invalid container mapping', - array('context' => $local_ctx)); - } - if($mapping->reverse && $container !== '@index' && - $container !== '@set' && $container !== null) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context @container value for a @reverse ' + - 'type definition must be @index or @set.', - 'jsonld.SyntaxError', 'invalid reverse property', - array('context' => $local_ctx)); - } - - // add @container to mapping - $mapping->{'@container'} = $container; - } - - if(property_exists($value, '@language') && - !property_exists($value, '@type')) { - $language = $value->{'@language'}; - if($language !== null && !is_string($language)) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context @language value must be ' . - 'a string or null.', 'jsonld.SyntaxError', - 'invalid language mapping', array('context' => $local_ctx)); - } - - // add @language to mapping - if($language !== null) { - $language = strtolower($language); - } - $mapping->{'@language'} = $language; - } - - // disallow aliasing @context and @preserve - $id = $mapping->{'@id'}; - if($id === '@context' || $id === '@preserve') { - throw new JsonLdException( - 'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.', - 'jsonld.SyntaxError', 'invalid keyword alias', - array('context' => $local_ctx)); - } - } - - /** - * Expands a string to a full IRI. The string may be a term, a prefix, a - * relative IRI, or an absolute IRI. The associated absolute IRI will be - * returned. - * - * @param stdClass $active_ctx the current active context. - * @param string $value the string to expand. - * @param assoc $relative_to options for how to resolve relative IRIs: - * base: true to resolve against the base IRI, false not to. - * vocab: true to concatenate after @vocab, false not to. - * @param stdClass $local_ctx the local context being processed (only given - * if called during document processing). - * @param defined a map for tracking cycles in context definitions (only given - * if called during document processing). - * - * @return mixed the expanded value. - */ - function _expandIri( - $active_ctx, $value, $relative_to=array(), $local_ctx=null, $defined=null) { - // already expanded - if($value === null || self::_isKeyword($value)) { - return $value; - } - - // define term dependency if not defined - if($local_ctx !== null && property_exists($local_ctx, $value) && - !self::_hasKeyValue($defined, $value, true)) { - $this->_createTermDefinition($active_ctx, $local_ctx, $value, $defined); - } - - if(isset($relative_to['vocab']) && $relative_to['vocab']) { - if(property_exists($active_ctx->mappings, $value)) { - $mapping = $active_ctx->mappings->{$value}; - - // value is explicitly ignored with a null mapping - if($mapping === null) { - return null; - } - - // value is a term - return $mapping->{'@id'}; - } - } - - // split value into prefix:suffix - $colon = strpos($value, ':'); - if($colon !== false) { - $prefix = substr($value, 0, $colon); - $suffix = substr($value, $colon + 1); - - // do not expand blank nodes (prefix of '_') or already-absolute - // IRIs (suffix of '//') - if($prefix === '_' || strpos($suffix, '//') === 0) { - return $value; - } - - // prefix dependency not defined, define it - if($local_ctx !== null && property_exists($local_ctx, $prefix)) { - $this->_createTermDefinition( - $active_ctx, $local_ctx, $prefix, $defined); - } - - // use mapping if prefix is defined - if(property_exists($active_ctx->mappings, $prefix)) { - $mapping = $active_ctx->mappings->{$prefix}; - if($mapping) { - return $mapping->{'@id'} . $suffix; - } - } - - // already absolute IRI - return $value; - } - - // prepend vocab - if(isset($relative_to['vocab']) && $relative_to['vocab'] && - property_exists($active_ctx, '@vocab')) { - return $active_ctx->{'@vocab'} . $value; - } - - // prepend base - $rval = $value; - if(isset($relative_to['base']) && $relative_to['base']) { - $rval = jsonld_prepend_base($active_ctx->{'@base'}, $rval); - } - - return $rval; - } - - /** - * Finds all @context URLs in the given JSON-LD input. - * - * @param mixed $input the JSON-LD input. - * @param stdClass $urls a map of URLs (url => false/@contexts). - * @param bool $replace true to replace the URLs in the given input with - * the @contexts from the urls map, false not to. - * @param string $base the base URL to resolve relative URLs with. - */ - protected function _findContextUrls($input, $urls, $replace, $base) { - if(is_array($input)) { - foreach($input as $e) { - $this->_findContextUrls($e, $urls, $replace, $base); - } - } else if(is_object($input)) { - foreach($input as $k => &$v) { - if($k !== '@context') { - $this->_findContextUrls($v, $urls, $replace, $base); - continue; - } - - // array @context - if(is_array($v)) { - $length = count($v); - for($i = 0; $i < $length; ++$i) { - if(is_string($v[$i])) { - $url = jsonld_prepend_base($base, $v[$i]); - // replace w/@context if requested - if($replace) { - $ctx = $urls->{$url}; - if(is_array($ctx)) { - // add flattened context - array_splice($v, $i, 1, $ctx); - $i += count($ctx) - 1; - $length = count($v); - } else { - $v[$i] = $ctx; - } - } else if(!property_exists($urls, $url)) { - // @context URL found - $urls->{$url} = false; - } - } - } - } else if(is_string($v)) { - // string @context - $v = jsonld_prepend_base($base, $v); - // replace w/@context if requested - if($replace) { - $input->{$k} = $urls->{$v}; - } else if(!property_exists($urls, $v)) { - // @context URL found - $urls->{$v} = false; - } - } - } - } - } - - /** - * Retrieves external @context URLs using the given document loader. Each - * instance of @context in the input that refers to a URL will be replaced - * with the JSON @context found at that URL. - * - * @param mixed $input the JSON-LD input with possible contexts. - * @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. - * - * @return mixed the result. - */ - protected function _retrieveContextUrls( - &$input, $cycles, $load_document, $base='') { - if(count(get_object_vars($cycles)) > self::MAX_CONTEXT_URLS) { - throw new JsonLdException( - 'Maximum number of @context URLs exceeded.', - 'jsonld.ContextUrlError', 'loading remote context failed', - array('max' => self::MAX_CONTEXT_URLS)); - } - - // for tracking the URLs to retrieve - $urls = new stdClass(); - - // regex for validating URLs - $regex = '/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/'; - - // find all URLs in the given input - $this->_findContextUrls($input, $urls, false, $base); - - // queue all unretrieved URLs - $queue = array(); - foreach($urls as $url => $ctx) { - if($ctx === false) { - // validate URL - if(!preg_match($regex, $url)) { - throw new JsonLdException( - 'Malformed or unsupported URL.', 'jsonld.InvalidUrl', - 'loading remote context failed', array('url' => $url)); - } - $queue[] = $url; - } - } - - // retrieve URLs in queue - foreach($queue as $url) { - // check for context URL cycle - if(property_exists($cycles, $url)) { - throw new JsonLdException( - 'Cyclical @context URLs detected.', - 'jsonld.ContextUrlError', 'recursive context inclusion', - array('url' => $url)); - } - $_cycles = self::copy($cycles); - $_cycles->{$url} = true; - - // retrieve URL - $remote_doc = call_user_func($load_document, $url); - $ctx = $remote_doc->document; - - // parse string context as JSON - if(is_string($ctx)) { - try { - $ctx = self::_parse_json($ctx); - } catch(Exception $e) { - throw new JsonLdException( - 'Could not parse JSON from URL.', - 'jsonld.ParseError', 'loading remote context failed', - array('url' => $url), $e); - } - } - - // ensure ctx is an object - if(!is_object($ctx)) { - throw new JsonLdException( - 'Derefencing a URL did not result in a valid JSON-LD object.', - 'jsonld.InvalidUrl', 'invalid remote context', array('url' => $url)); - } - - // use empty context if no @context key is present - if(!property_exists($ctx, '@context')) { - $ctx = (object)array('@context' => new stdClass()); - } else { - $ctx = (object)array('@context' => $ctx->{'@context'}); - } - - // append context URL to context if given - if($remote_doc->contextUrl !== null) { - $ctx->{'@context'} = self::arrayify($ctx->{'@context'}); - $ctx->{'@context'}[] = $remote_doc->contextUrl; - } - - // recurse - $this->_retrieveContextUrls($ctx, $_cycles, $load_document, $url); - $urls->{$url} = $ctx->{'@context'}; - } - - // replace all URLS in the input - $this->_findContextUrls($input, $urls, true, $base); - } - - /** - * Gets the initial context. - * - * @param assoc $options the options to use. - * base the document base IRI. - * - * @return stdClass the initial context. - */ - protected function _getInitialContext($options) { - return (object)array( - '@base' => jsonld_parse_url($options['base']), - 'mappings' => new stdClass(), - 'inverse' => null); - } - - /** - * Generates an inverse context for use in the compaction algorithm, if - * not already generated for the given active context. - * - * @param stdClass $active_ctx the active context to use. - * - * @return stdClass the inverse context. - */ - protected function _getInverseContext($active_ctx) { - // inverse context already generated - if($active_ctx->inverse) { - return $active_ctx->inverse; - } - - $inverse = $active_ctx->inverse = new stdClass(); - - // variables for building fast CURIE map - $fast_curie_map = $active_ctx->fast_curie_map = new ArrayObject(); - $iris_to_terms = array(); - - // handle default language - $default_language = '@none'; - if(property_exists($active_ctx, '@language')) { - $default_language = $active_ctx->{'@language'}; - } - - // create term selections for each mapping in the context, ordered by - // shortest and then lexicographically least - $mappings = $active_ctx->mappings; - $terms = array_keys((array)$mappings); - usort($terms, array($this, '_compareShortestLeast')); - foreach($terms as $term) { - $mapping = $mappings->{$term}; - if($mapping === null) { - continue; - } - - // add term selection where it applies - if(property_exists($mapping, '@container')) { - $container = $mapping->{'@container'}; - } else { - $container = '@none'; - } - - // iterate over every IRI in the mapping - $iris = $mapping->{'@id'}; - $iris = self::arrayify($iris); - foreach($iris as $iri) { - $is_keyword = self::_isKeyword($iri); - - // initialize container map - if(!property_exists($inverse, $iri)) { - $inverse->{$iri} = new stdClass(); - if(!$is_keyword && !$mapping->_term_has_colon) { - // init IRI to term map and fast CURIE map - $iris_to_terms[$iri] = new ArrayObject(); - $iris_to_terms[$iri][] = $term; - $fast_curie_entry = (object)array( - 'iri' => $iri, 'terms' => $iris_to_terms[$iri]); - if(!array_key_exists($iri[0], (array)$fast_curie_map)) { - $fast_curie_map[$iri[0]] = new ArrayObject(); - } - $fast_curie_map[$iri[0]][] = $fast_curie_entry; - } - } else if(!$is_keyword && !$mapping->_term_has_colon) { - // add IRI to term match - $iris_to_terms[$iri][] = $term; - } - $container_map = $inverse->{$iri}; - - // add new entry - if(!property_exists($container_map, $container)) { - $container_map->{$container} = (object)array( - '@language' => new stdClass(), - '@type' => new stdClass()); - } - $entry = $container_map->{$container}; - - if($mapping->reverse) { - // term is preferred for values using @reverse - $this->_addPreferredTerm( - $mapping, $term, $entry->{'@type'}, '@reverse'); - } else if(property_exists($mapping, '@type')) { - // term is preferred for values using specific type - $this->_addPreferredTerm( - $mapping, $term, $entry->{'@type'}, $mapping->{'@type'}); - } else if(property_exists($mapping, '@language')) { - // term is preferred for values using specific language - $language = $mapping->{'@language'}; - if($language === null) { - $language = '@null'; - } - $this->_addPreferredTerm( - $mapping, $term, $entry->{'@language'}, $language); - } else { - // term is preferred for values w/default language or no type and - // no language - // add an entry for the default language - $this->_addPreferredTerm( - $mapping, $term, $entry->{'@language'}, $default_language); - - // add entries for no type and no language - $this->_addPreferredTerm( - $mapping, $term, $entry->{'@type'}, '@none'); - $this->_addPreferredTerm( - $mapping, $term, $entry->{'@language'}, '@none'); - } - } - } - - // build fast CURIE map - foreach($fast_curie_map as $key => $value) { - $this->_buildIriMap($fast_curie_map, $key, 1); - } - - return $inverse; - } - - /** - * Runs a recursive algorithm to build a lookup map for quickly finding - * potential CURIEs. - * - * @param ArrayObject $iri_map the map to build. - * @param string $key the current key in the map to work on. - * @param int $idx the index into the IRI to compare. - */ - function _buildIriMap($iri_map, $key, $idx) { - $entries = $iri_map[$key]; - $next = $iri_map[$key] = new ArrayObject(); - - foreach($entries as $entry) { - $iri = $entry->iri; - if($idx >= strlen($iri)) { - $letter = ''; - } else { - $letter = $iri[$idx]; - } - if(!isset($next[$letter])) { - $next[$letter] = new ArrayObject(); - } - $next[$letter][] = $entry; - } - - foreach($next as $key => $value) { - if($key === '') { - continue; - } - $this->_buildIriMap($next, $key, $idx + 1); - } - } - - /** - * Adds the term for the given entry if not already added. - * - * @param stdClass $mapping the term mapping. - * @param string $term the term to add. - * @param stdClass $entry the inverse context type_or_language entry to - * add to. - * @param string $type_or_language_value the key in the entry to add to. - */ - function _addPreferredTerm($mapping, $term, $entry, $type_or_language_value) { - if(!property_exists($entry, $type_or_language_value)) { - $entry->{$type_or_language_value} = $term; - } - } - - /** - * Clones an active context, creating a child active context. - * - * @return stdClass a clone (child) of the active context. - */ - protected function _cloneActiveContext($active_ctx) { - $child = new stdClass(); - $child->{'@base'} = $active_ctx->{'@base'}; - $child->mappings = self::copy($active_ctx->mappings); - $child->inverse = null; - if(property_exists($active_ctx, '@language')) { - $child->{'@language'} = $active_ctx->{'@language'}; - } - if(property_exists($active_ctx, '@vocab')) { - $child->{'@vocab'} = $active_ctx->{'@vocab'}; - } - return $child; - } - - /** - * Returns whether or not the given value is a keyword. - * - * @param string $v the value to check. - * - * @return bool true if the value is a keyword, false if not. - */ - protected static function _isKeyword($v) { - if(!is_string($v)) { - return false; - } - switch($v) { - case '@base': - case '@context': - case '@container': - case '@default': - case '@embed': - case '@explicit': - case '@graph': - case '@id': - case '@index': - case '@language': - case '@list': - case '@omitDefault': - case '@preserve': - case '@requireAll': - case '@reverse': - case '@set': - case '@type': - case '@value': - case '@vocab': - return true; - } - return false; - } - - /** - * Returns true if the given value is an empty Object. - * - * @param mixed $v the value to check. - * - * @return bool true if the value is an empty Object, false if not. - */ - protected static function _isEmptyObject($v) { - return is_object($v) && count(get_object_vars($v)) === 0; - } - - /** - * Throws an exception if the given value is not a valid @type value. - * - * @param mixed $v the value to check. - */ - protected static function _validateTypeValue($v) { - // must be a string or empty object - if(is_string($v) || self::_isEmptyObject($v)) { - return; - } - - // must be an array - $is_valid = false; - if(is_array($v)) { - // must contain only strings - $is_valid = true; - foreach($v as $e) { - if(!(is_string($e))) { - $is_valid = false; - break; - } - } - } - - if(!$is_valid) { - throw new JsonLdException( - 'Invalid JSON-LD syntax; "@type" value must a string, an array ' . - 'of strings, or an empty object.', - 'jsonld.SyntaxError', 'invalid type value', array('value' => $v)); - } - } - - /** - * Returns true if the given value is a subject with properties. - * - * @param mixed $v the value to check. - * - * @return bool true if the value is a subject with properties, false if not. - */ - protected static function _isSubject($v) { - // Note: A value is a subject if all of these hold true: - // 1. It is an Object. - // 2. It is not a @value, @set, or @list. - // 3. It has more than 1 key OR any existing key is not @id. - $rval = false; - if(is_object($v) && - !property_exists($v, '@value') && - !property_exists($v, '@set') && - !property_exists($v, '@list')) { - $count = count(get_object_vars($v)); - $rval = ($count > 1 || !property_exists($v, '@id')); - } - return $rval; - } - - /** - * Returns true if the given value is a subject reference. - * - * @param mixed $v the value to check. - * - * @return bool true if the value is a subject reference, false if not. - */ - protected static function _isSubjectReference($v) { - // Note: A value is a subject reference if all of these hold true: - // 1. It is an Object. - // 2. It has a single key: @id. - return (is_object($v) && count(get_object_vars($v)) === 1 && - property_exists($v, '@id')); - } - - /** - * Returns true if the given value is a @value. - * - * @param mixed $v the value to check. - * - * @return bool true if the value is a @value, false if not. - */ - protected static function _isValue($v) { - // Note: A value is a @value if all of these hold true: - // 1. It is an Object. - // 2. It has the @value property. - return is_object($v) && property_exists($v, '@value'); - } - - /** - * Returns true if the given value is a @list. - * - * @param mixed $v the value to check. - * - * @return bool true if the value is a @list, false if not. - */ - protected static function _isList($v) { - // Note: A value is a @list if all of these hold true: - // 1. It is an Object. - // 2. It has the @list property. - return is_object($v) && property_exists($v, '@list'); - } - - /** - * Returns true if the given value is a blank node. - * - * @param mixed $v the value to check. - * - * @return bool true if the value is a blank node, false if not. - */ - protected static function _isBlankNode($v) { - // Note: A value is a blank node if all of these hold true: - // 1. It is an Object. - // 2. If it has an @id key its value begins with '_:'. - // 3. It has no keys OR is not a @value, @set, or @list. - $rval = false; - if(is_object($v)) { - if(property_exists($v, '@id')) { - $rval = (strpos($v->{'@id'}, '_:') === 0); - } else { - $rval = (count(get_object_vars($v)) === 0 || - !(property_exists($v, '@value') || - property_exists($v, '@set') || - property_exists($v, '@list'))); - } - } - return $rval; - } - - /** - * Returns true if the given value is an absolute IRI, false if not. - * - * @param string $v the value to check. - * - * @return bool true if the value is an absolute IRI, false if not. - */ - protected static function _isAbsoluteIri($v) { - return strpos($v, ':') !== false; - } - - /** - * Returns true if the given target has the given key and its - * value equals is the given value. - * - * @param stdClass $target the target object. - * @param string key the key to check. - * @param mixed $value the value to check. - * - * @return bool true if the target has the given key and its value matches. - */ - protected static function _hasKeyValue($target, $key, $value) { - return (property_exists($target, $key) && $target->{$key} === $value); - } - - /** - * Returns true if both of the given objects have the same value for the - * given key or if neither of the objects contain the given key. - * - * @param stdClass $o1 the first object. - * @param stdClass $o2 the second object. - * @param string key the key to check. - * - * @return bool true if both objects have the same value for the key or - * neither has the key. - */ - protected static function _compareKeyValues($o1, $o2, $key) { - if(property_exists($o1, $key)) { - return property_exists($o2, $key) && $o1->{$key} === $o2->{$key}; - } - return !property_exists($o2, $key); - } - - /** - * Parses JSON and sets an appropriate exception message on error. - * - * @param string $json the JSON to parse. - * - * @return mixed the parsed JSON object or array. - */ - protected static function _parse_json($json) { - $rval = json_decode($json); - $error = json_last_error(); - if($error === JSON_ERROR_NONE && $rval === null) { - $error = JSON_ERROR_SYNTAX; - } - switch($error) { - case JSON_ERROR_NONE: - break; - case JSON_ERROR_DEPTH: - throw new JsonLdException( - 'Could not parse JSON; the maximum stack depth has been exceeded.', - 'jsonld.ParseError'); - case JSON_ERROR_STATE_MISMATCH: - throw new JsonLdException( - 'Could not parse JSON; invalid or malformed JSON.', - 'jsonld.ParseError'); - case JSON_ERROR_CTRL_CHAR: - case JSON_ERROR_SYNTAX: - throw new JsonLdException( - 'Could not parse JSON; syntax error, malformed JSON.', - 'jsonld.ParseError'); - case JSON_ERROR_UTF8: - throw new JsonLdException( - 'Could not parse JSON from URL; malformed UTF-8 characters.', - 'jsonld.ParseError'); - default: - throw new JsonLdException( - 'Could not parse JSON from URL; unknown error.', - 'jsonld.ParseError'); - } - return $rval; - } +class JsonLdProcessor +{ + + /** XSD constants */ + const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean'; + const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double'; + const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer'; + const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string'; + + /** RDF constants */ + 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'; + const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'; + const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'; + 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; + + /** Processor-specific RDF dataset parsers. */ + protected $rdfParsers = null; + + /** + * Constructs a JSON-LD processor. + */ + 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. + * [base] the base IRI to use. + * [compactArrays] true to compact arrays to single values when + * appropriate, false not to (default: true). + * [graph] true to always output a top-level graph (default: false). + * [skipExpansion] true to assume the input is expanded and skip + * expansion, false not to, defaults to false. + * [activeCtx] true to also return the active context used. + * [documentLoader(url)] the document loader. + * + * @return mixed the compacted JSON-LD output. + */ + public function compact($input, $ctx, $options) + { + global $jsonld_default_load_document; + + if ($ctx === null) { + throw new JsonLdException( + 'The compaction context must not be null.', 'jsonld.CompactError', 'invalid local context'); + } + + // nothing to compact + if ($input === null) { + return null; + } + + self::setdefaults($options, [ + 'base' => is_string($input) ? $input : '', + 'compactArrays' => true, + 'graph' => false, + 'skipExpansion' => false, + 'activeCtx' => false, + 'documentLoader' => $jsonld_default_load_document, + 'link' => false]); + if ($options['link']) { + // force skip expansion when linking, "link" is not part of the + // public API, it should only be called from framing + $options['skipExpansion'] = true; + } + + if ($options['skipExpansion'] === true) { + $expanded = $input; + } else { + // expand input + try { + $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); + } 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); + + if ($options['compactArrays'] && + !$options['graph'] && is_array($compacted)) { + if (count($compacted) === 1) { + // simplify to a single item + $compacted = $compacted[0]; + } else if (count($compacted) === 0) { + // simplify to an empty object + $compacted = new stdClass(); + } + } 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'}; + } + + // build output context + $ctx = self::copy($ctx); + $ctx = self::arrayify($ctx); + + // remove empty contexts + $tmp = $ctx; + $ctx = []; + 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); + if ($ctx_length === 1) { + $ctx = $ctx[0]; + } + + // add context and/or @graph + if (is_array($compacted)) { + // use '@graph' keyword + $kwgraph = $this->_compactIri($active_ctx, '@graph'); + $graph = $compacted; + $compacted = new stdClass(); + if ($has_context) { + $compacted->{'@context'} = $ctx; + } + $compacted->{$kwgraph} = $graph; + } else if (is_object($compacted) && $has_context) { + // reorder keys so @context is first + $graph = $compacted; + $compacted = new stdClass(); + $compacted->{'@context'} = $ctx; + foreach ($graph as $k => $v) { + $compacted->{$k} = $v; + } + } + + if ($options['activeCtx']) { + return ['compacted' => $compacted, 'activeCtx' => $active_ctx]; + } + + return $compacted; + } + + /** + * Performs JSON-LD expansion. + * + * @param mixed $input the JSON-LD object to expand. + * @param assoc $options the options to use: + * [base] the base IRI to use. + * [expandContext] a context to expand with. + * [keepFreeFloatingNodes] true to keep free-floating nodes, + * false not to, defaults to false. + * [documentLoader(url)] the document loader. + * + * @return array the expanded JSON-LD output. + */ + public function expand($input, $options) + { + global $jsonld_default_load_document; + self::setdefaults($options, [ + 'keepFreeFloatingNodes' => false, + 'documentLoader' => $jsonld_default_load_document]); + + // if input is a string, attempt to dereference remote document + if (is_string($input)) { + $remote_doc = call_user_func($options['documentLoader'], $input); + } else { + $remote_doc = (object) [ + 'contextUrl' => null, + 'documentUrl' => null, + 'document' => $input]; + } + + try { + if ($remote_doc->document === null) { + throw new JsonLdException( + 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument'); + } + if (is_string($remote_doc->document)) { + $remote_doc->document = self::_parse_json($remote_doc->document); + } + } catch (Exception $e) { + throw new JsonLdException( + 'Could not retrieve a JSON-LD document from the URL.', 'jsonld.LoadDocumentError', 'loading document failed', ['remoteDoc' => $remote_doc], $e); + } + + // set default base + self::setdefault($options, 'base', $remote_doc->documentUrl ?: ''); + + // build meta-object and retrieve all @context urls + $input = (object) [ + 'document' => self::copy($remote_doc->document), + 'remoteContext' => (object) [ + '@context' => $remote_doc->contextUrl]]; + if (isset($options['expandContext'])) { + $expand_context = self::copy($options['expandContext']); + if (is_object($expand_context) && + property_exists($expand_context, '@context')) { + $input->expandContext = $expand_context; + } else { + $input->expandContext = (object) ['@context' => $expand_context]; + } + } + + // retrieve all @context URLs in the input + try { + $this->_retrieveContextUrls( + $input, new stdClass(), $options['documentLoader'], $options['base']); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not perform JSON-LD expansion.', 'jsonld.ExpandError', null, null, $e); + } + + $active_ctx = $this->_getInitialContext($options); + $document = $input->document; + $remote_context = $input->remoteContext->{'@context'}; + + // process optional expandContext + if (property_exists($input, 'expandContext')) { + $active_ctx = self::_processContext( + $active_ctx, $input->expandContext, $options); + } + + // process remote context from HTTP Link Header + if ($remote_context) { + $active_ctx = self::_processContext( + $active_ctx, $remote_context, $options); + } + + // do expansion + $expanded = $this->_expand($active_ctx, null, $document, $options, false); + + // optimize away @graph with no other properties + if (is_object($expanded) && property_exists($expanded, '@graph') && + count(array_keys((array) $expanded)) === 1) { + $expanded = $expanded->{'@graph'}; + } else if ($expanded === null) { + $expanded = []; + } + // normalize to an array + return self::arrayify($expanded); + } + + /** + * Performs JSON-LD flattening. + * + * @param mixed $input the JSON-LD to flatten. + * @param ctx the context to use to compact the flattened output, or null. + * @param assoc $options the options to use: + * [base] the base IRI to use. + * [expandContext] a context to expand with. + * [documentLoader(url)] the document loader. + * + * @return array the flattened output. + */ + public function flatten($input, $ctx, $options) + { + global $jsonld_default_load_document; + self::setdefaults($options, [ + 'base' => is_string($input) ? $input : '', + 'documentLoader' => $jsonld_default_load_document]); + + try { + // expand input + $expanded = $this->expand($input, $options); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not expand input before flattening.', 'jsonld.FlattenError', null, null, $e); + } + + // do flattening + $flattened = $this->_flatten($expanded); + + if ($ctx === null) { + return $flattened; + } + + // compact result (force @graph option to true, skip expansion) + $options['graph'] = true; + $options['skipExpansion'] = true; + try { + $compacted = $this->compact($flattened, $ctx, $options); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not compact flattened output.', 'jsonld.FlattenError', null, null, $e); + } + + return $compacted; + } + + /** + * Performs JSON-LD framing. + * + * @param mixed $input the JSON-LD object to frame. + * @param stdClass $frame the JSON-LD frame to use. + * @param $options the framing options. + * [base] the base IRI to use. + * [expandContext] a context to expand with. + * [embed] default @embed flag: '@last', '@always', '@never', '@link' + * (default: '@last'). + * [explicit] default @explicit flag (default: false). + * [requireAll] default @requireAll flag (default: true). + * [omitDefault] default @omitDefault flag (default: false). + * [documentLoader(url)] the document loader. + * + * @return stdClass the framed JSON-LD output. + */ + public function frame($input, $frame, $options) + { + global $jsonld_default_load_document; + self::setdefaults($options, [ + 'base' => is_string($input) ? $input : '', + 'compactArrays' => true, + 'embed' => '@last', + 'explicit' => false, + 'requireAll' => true, + 'omitDefault' => false, + 'documentLoader' => $jsonld_default_load_document]); + + // if frame is a string, attempt to dereference remote document + if (is_string($frame)) { + $remote_frame = call_user_func($options['documentLoader'], $frame); + } else { + $remote_frame = (object) [ + 'contextUrl' => null, + 'documentUrl' => null, + 'document' => $frame]; + } + + try { + if ($remote_frame->document === null) { + throw new JsonLdException( + '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); + } + } catch (Exception $e) { + throw new JsonLdException( + 'Could not retrieve a JSON-LD document from the URL.', 'jsonld.LoadDocumentError', 'loading document failed', ['remoteDoc' => $remote_frame], $e); + } + + // preserve frame context + $frame = $remote_frame->document; + if ($frame !== null) { + $ctx = (property_exists($frame, '@context') ? + $frame->{'@context'} : new stdClass()); + if ($remote_frame->contextUrl !== null) { + if ($ctx !== null) { + $ctx = $remote_frame->contextUrl; + } else { + $ctx = self::arrayify($ctx); + $ctx[] = $remote_frame->contextUrl; + } + $frame->{'@context'} = $ctx; + } + } + + try { + // expand input + $expanded = $this->expand($input, $options); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not expand input before framing.', 'jsonld.FrameError', null, null, $e); + } + + try { + // expand frame + $opts = $options; + $opts['keepFreeFloatingNodes'] = true; + $expanded_frame = $this->expand($frame, $opts); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not expand frame before framing.', 'jsonld.FrameError', null, null, $e); + } + + // do framing + $framed = $this->_frame($expanded, $expanded_frame, $options); + + try { + // compact result (force @graph option to true, skip expansion, check + // for linked embeds) + $options['graph'] = true; + $options['skipExpansion'] = true; + $options['link'] = new ArrayObject(); + $options['activeCtx'] = true; + $result = $this->compact($framed, $ctx, $options); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not compact framed output.', 'jsonld.FrameError', null, null, $e); + } + + $compacted = $result['compacted']; + $active_ctx = $result['activeCtx']; + + // get graph alias + $graph = $this->_compactIri($active_ctx, '@graph'); + // remove @preserve from results + $options['link'] = new ArrayObject(); + $compacted->{$graph} = $this->_removePreserve( + $active_ctx, $compacted->{$graph}, $options); + return $compacted; + } + + /** + * Performs JSON-LD normalization. + * + * @param mixed $input the JSON-LD object to normalize. + * @param assoc $options the options to use: + * [base] the base IRI to use. + * [expandContext] a context to expand with. + * [inputFormat] the format if input is not JSON-LD: + * 'application/nquads' for N-Quads. + * [format] the format if output is a string: + * 'application/nquads' for N-Quads. + * [documentLoader(url)] the document loader. + * + * @return mixed the normalized output. + */ + public function normalize($input, $options) + { + global $jsonld_default_load_document; + self::setdefaults($options, [ + 'base' => is_string($input) ? $input : '', + 'documentLoader' => $jsonld_default_load_document]); + + if (isset($options['inputFormat'])) { + if ($options['inputFormat'] != 'application/nquads') { + throw new JsonLdException( + 'Unknown normalization input format.', 'jsonld.NormalizeError'); + } + $dataset = $this->parseNQuads($input); + } else { + try { + // convert to RDF dataset then do normalization + $opts = $options; + if (isset($opts['format'])) { + unset($opts['format']); + } + $opts['produceGeneralizedRdf'] = false; + $dataset = $this->toRDF($input, $opts); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not convert input to RDF dataset before normalization.', 'jsonld.NormalizeError', null, null, $e); + } + } + + // do normalization + return $this->_normalize($dataset, $options); + } + + /** + * Converts an RDF dataset to JSON-LD. + * + * @param mixed $dataset a serialized string of RDF in a format specified + * 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). + * [useRdfType] true to use rdf:type, false to use @type + * (default: false). + * [useNativeTypes] true to convert XSD types into native types + * (boolean, integer, double), false not to (default: false). + * + * @return array the JSON-LD output. + */ + public function fromRDF($dataset, $options) + { + global $jsonld_rdf_parsers; + + self::setdefaults($options, [ + 'useRdfType' => false, + 'useNativeTypes' => false]); + + if (!isset($options['format']) && is_string($dataset)) { + // set default format to nquads + $options['format'] = 'application/nquads'; + } + + // handle special format + if (isset($options['format']) && $options['format']) { + // supported formats (processor-specific and global) + if (($this->rdfParsers !== null && + !property_exists($this->rdfParsers, $options['format'])) || + $this->rdfParsers === null && + !property_exists($jsonld_rdf_parsers, $options['format'])) { + throw new JsonLdException( + 'Unknown input format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]); + } + if ($this->rdfParsers !== null) { + $callable = $this->rdfParsers->{$options['format']}; + } else { + $callable = $jsonld_rdf_parsers->{$options['format']}; + } + $dataset = call_user_func($callable, $dataset); + } + + // convert from RDF + return $this->_fromRDF($dataset, $options); + } + + /** + * Outputs the RDF dataset found in the given JSON-LD object. + * + * @param mixed $input the JSON-LD object. + * @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: + * 'application/nquads' for N-Quads. + * [produceGeneralizedRdf] true to output generalized RDF, false + * to produce only standard RDF (default: false). + * [documentLoader(url)] the document loader. + * + * @return mixed the resulting RDF dataset (or a serialization of it). + */ + public function toRDF($input, $options) + { + global $jsonld_default_load_document; + self::setdefaults($options, [ + 'base' => is_string($input) ? $input : '', + 'produceGeneralizedRdf' => false, + 'documentLoader' => $jsonld_default_load_document]); + + try { + // expand input + $expanded = $this->expand($input, $options); + } catch (JsonLdException $e) { + throw new JsonLdException( + 'Could not expand input before serialization to RDF.', 'jsonld.RdfError', null, null, $e); + } + + // create node map for default graph (and any named graphs) + $namer = new UniqueNamer('_:b'); + $node_map = (object) ['@default' => new stdClass()]; + $this->_createNodeMap($expanded, $node_map, '@default', $namer); + + // output RDF dataset + $dataset = new stdClass(); + $graph_names = array_keys((array) $node_map); + sort($graph_names); + foreach ($graph_names as $graph_name) { + $graph = $node_map->{$graph_name}; + // skip relative IRIs + if ($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) { + $dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options); + } + } + + $rval = $dataset; + + // convert to output format + if (isset($options['format']) && $options['format']) { + // supported formats + if ($options['format'] === 'application/nquads') { + $rval = self::toNQuads($dataset); + } else { + throw new JsonLdException( + 'Unknown output format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]); + } + } + + return $rval; + } + + /** + * 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. + * @param mixed $local_ctx the local context to process. + * @param assoc $options the options to use: + * [documentLoader(url)] the document loader. + * + * @return stdClass the new active context. + */ + public function processContext($active_ctx, $local_ctx, $options) + { + global $jsonld_default_load_document; + self::setdefaults($options, [ + 'base' => '', + 'documentLoader' => $jsonld_default_load_document]); + + // return initial context early for null context + if ($local_ctx === null) { + return $this->_getInitialContext($options); + } + + // retrieve URLs in local_ctx + $local_ctx = self::copy($local_ctx); + if (is_string($local_ctx) or ( + is_object($local_ctx) && !property_exists($local_ctx, '@context'))) { + $local_ctx = (object) ['@context' => $local_ctx]; + } + try { + $this->_retrieveContextUrls( + $local_ctx, new stdClass(), $options['documentLoader'], $options['base']); + } catch (Exception $e) { + throw new JsonLdException( + 'Could not process JSON-LD context.', 'jsonld.ContextError', null, null, $e); + } + + // process context + return $this->_processContext($active_ctx, $local_ctx, $options); + } + + /** + * Returns true if the given subject has the given property. + * + * @param stdClass $subject the subject to check. + * @param string $property the property to look for. + * + * @return bool true if the subject has the given property, false if not. + */ + public static function hasProperty($subject, $property) + { + $rval = false; + if (property_exists($subject, $property)) { + $value = $subject->{$property}; + $rval = (!is_array($value) || count($value) > 0); + } + return $rval; + } + + /** + * Determines if the given value is a property of the given subject. + * + * @param stdClass $subject the subject to check. + * @param string $property the property to check. + * @param mixed $value the value to check. + * + * @return bool true if the value exists, false if not. + */ + public static function hasValue($subject, $property, $value) + { + $rval = false; + if (self::hasProperty($subject, $property)) { + $val = $subject->{$property}; + $is_list = self::_isList($val); + if (is_array($val) || $is_list) { + if ($is_list) { + $val = $val->{'@list'}; + } + foreach ($val as $v) { + if (self::compareValues($value, $v)) { + $rval = true; + break; + } + } + } else if (!is_array($value)) { + // avoid matching the set of values with an array value parameter + $rval = self::compareValues($value, $val); + } + } + return $rval; + } + + /** + * Adds a value to a subject. If the value is an array, all values in the + * array will be added. + * + * 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. + * + * @param stdClass $subject the subject to add the value to. + * @param string $property the property that relates the value to the subject. + * @param mixed $value the value to add. + * @param assoc [$options] the options to use: + * [propertyIsArray] true if the property is always an array, false + * if not (default: false). + * [allowDuplicate] true to allow duplicates, false not to (uses a + * simple shallow comparison of subject ID or value) + * (default: true). + */ + public static function addValue( + $subject, $property, $value, $options = []) + { + self::setdefaults($options, [ + 'allowDuplicate' => true, + 'propertyIsArray' => false]); + + if (is_array($value)) { + if (count($value) === 0 && $options['propertyIsArray'] && + !property_exists($subject, $property)) { + $subject->{$property} = []; + } + foreach ($value as $v) { + self::addValue($subject, $property, $v, $options); + } + } else if (property_exists($subject, $property)) { + // check if subject already has value if duplicates not allowed + $has_value = (!$options['allowDuplicate'] && + self::hasValue($subject, $property, $value)); + + // make property an array if value not present or always an array + if (!is_array($subject->{$property}) && + (!$has_value || $options['propertyIsArray'])) { + $subject->{$property} = [$subject->{$property}]; + } + + // add new value + if (!$has_value) { + $subject->{$property}[] = $value; + } + } else { + // add new value as set or single value + $subject->{$property} = ($options['propertyIsArray'] ? + [$value] : $value); + } + } + + /** + * Gets all of the values for a subject's property as an array. + * + * @param stdClass $subject the subject. + * @param string $property the property. + * + * @return array all of the values for a subject's property as an array. + */ + public static function getValues($subject, $property) + { + $rval = (property_exists($subject, $property) ? + $subject->{$property} : []); + return self::arrayify($rval); + } + + /** + * Removes a property from a subject. + * + * @param stdClass $subject the subject. + * @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. + * @param mixed $value the value to remove. + * @param assoc [$options] the options to use: + * [propertyIsArray] true if the property is always an array, + * false if not (default: false). + */ + public static function removeValue( + $subject, $property, $value, $options = []) + { + self::setdefaults($options, [ + 'propertyIsArray' => false]); + + // filter out value + $filter = function($e) use ($value) { + return !self::compareValues($e, $value); + }; + $values = self::getValues($subject, $property); + $values = array_values(array_filter($values, $filter)); + + if (count($values) === 0) { + self::removeProperty($subject, $property); + } else if (count($values) === 1 && !$options['propertyIsArray']) { + $subject->{$property} = $values[0]; + } else { + $subject->{$property} = $values; + } + } + + /** + * Compares two JSON-LD values for equality. Two JSON-LD values will be + * considered equal if: + * + * 1. They are both primitives of the same type and value. + * 2. They are both @values with the same @value, @type, @language, + * and @index, OR + * 3. They both have @ids that are the same. + * + * @param mixed $v1 the first value. + * @param mixed $v2 the second value. + * + * @return bool true if v1 and v2 are considered equal, false if not. + */ + public static function compareValues($v1, $v2) + { + // 1. equal primitives + if ($v1 === $v2) { + return true; + } + + // 2. equal @values + if (self::_isValue($v1) && self::_isValue($v2)) { + return ( + self::_compareKeyValues($v1, $v2, '@value') && + self::_compareKeyValues($v1, $v2, '@type') && + self::_compareKeyValues($v1, $v2, '@language') && + self::_compareKeyValues($v1, $v2, '@index')); + } + + // 3. equal @ids + if (is_object($v1) && property_exists($v1, '@id') && + is_object($v2) && property_exists($v2, '@id')) { + return $v1->{'@id'} === $v2->{'@id'}; + } + + return false; + } + + /** + * 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 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. + */ + public static function getContextValue($ctx, $key, $type) + { + $rval = null; + + // return null for invalid key + if ($key === null) { + return $rval; + } + + // get default language + if ($type === '@language' && property_exists($ctx, $type)) { + $rval = $ctx->{$type}; + } + + // get specific entry information + if (property_exists($ctx->mappings, $key)) { + $entry = $ctx->mappings->{$key}; + if ($entry === null) { + return null; + } + + if ($type === null) { + // return whole entry + $rval = $entry; + } else if (property_exists($entry, $type)) { + // return entry value for type + $rval = $entry->{$type}; + } + } + + return $rval; + } + + /** + * Parses RDF in the form of N-Quads. + * + * @param string $input the N-Quads input to parse. + * + * @return stdClass an RDF dataset. + */ + public static function parseNQuads($input) + { + // define partial regexes + $iri = '(?:<([^:]+:[^>]*)>)'; + $bnode = '(_:(?:[A-Za-z][A-Za-z0-9]*))'; + $plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"'; + $datatype = "(?:\\^\\^$iri)"; + $language = '(?:@([a-z]+(?:-[a-z0-9]+)*))'; + $literal = "(?:$plain(?:$datatype|$language)?)"; + $ws = '[ \\t]'; + $eoln = '/(?:\r\n)|(?:\n)|(?:\r)/'; + $empty = "/^$ws*$/"; + + // define quad part regexes + $subject = "(?:$iri|$bnode)$ws+"; + $property = "$iri$ws+"; + $object = "(?:$iri|$bnode|$literal)$ws*"; + $graph_name = "(?:\\.|(?:(?:$iri|$bnode)$ws*\\.))"; + + // full quad regex + $quad = "/^$ws*$subject$property$object$graph_name$ws*$/"; + + // build RDF dataset + $dataset = new stdClass(); + + // split N-Quad input into lines + $lines = preg_split($eoln, $input); + $line_number = 0; + foreach ($lines as $line) { + $line_number += 1; + + // skip empty lines + if (preg_match($empty, $line)) { + continue; + } + + // parse quad + if (!preg_match($quad, $line, $match)) { + throw new JsonLdException( + 'Error while parsing N-Quads; invalid quad.', 'jsonld.ParseError', null, ['line' => $line_number]); + } + + // create RDF triple + $triple = (object) [ + 'subject' => new stdClass(), + 'predicate' => new stdClass(), + 'object' => new stdClass()]; + + // get subject + if ($match[1] !== '') { + $triple->subject->type = 'IRI'; + $triple->subject->value = $match[1]; + } else { + $triple->subject->type = 'blank node'; + $triple->subject->value = $match[2]; + } + + // get predicate + $triple->predicate->type = 'IRI'; + $triple->predicate->value = $match[3]; + + // get object + if ($match[4] !== '') { + $triple->object->type = 'IRI'; + $triple->object->value = $match[4]; + } else if ($match[5] !== '') { + $triple->object->type = 'blank node'; + $triple->object->value = $match[5]; + } else { + $triple->object->type = 'literal'; + $unescaped = str_replace( + ['\"', '\t', '\n', '\r', '\\\\'], ['"', "\t", "\n", "\r", '\\'], $match[6]); + if (isset($match[7]) && $match[7] !== '') { + $triple->object->datatype = $match[7]; + } else if (isset($match[8]) && $match[8] !== '') { + $triple->object->datatype = self::RDF_LANGSTRING; + $triple->object->language = $match[8]; + } else { + $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] !== '') { + $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]; + } else { + // add triple if unique to its graph + $unique = true; + $triples = &$dataset->{$name}; + foreach ($triples as $t) { + if (self::_compareRDFTriples($t, $triple)) { + $unique = false; + break; + } + } + if ($unique) { + $triples[] = $triple; + } + } + } + + return $dataset; + } + + /** + * Converts an RDF dataset to N-Quads. + * + * @param stdClass $dataset the RDF dataset to convert. + * + * @return string the N-Quads string. + */ + public static function toNQuads($dataset) + { + $quads = []; + foreach ($dataset as $graph_name => $triples) { + foreach ($triples as $triple) { + if ($graph_name === '@default') { + $graph_name = null; + } + $quads[] = self::toNQuad($triple, $graph_name); + } + } + sort($quads); + return implode($quads); + } + + /** + * 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 + * for the default graph. + * @param string $bnode the bnode the quad is mapped to (optional, for + * use during normalization only). + * + * @return string the N-Quad string. + */ + public static function toNQuad($triple, $graph_name, $bnode = null) + { + $s = $triple->subject; + $p = $triple->predicate; + $o = $triple->object; + $g = $graph_name; + + $quad = ''; + + // subject is an IRI + if ($s->type === 'IRI') { + $quad .= "<{$s->value}>"; + } else if ($bnode !== null) { + // bnode normalization mode + $quad .= ($s->value === $bnode) ? '_:a' : '_:z'; + } else { + // bnode normal mode + $quad .= $s->value; + } + $quad .= ' '; + + // predicate is an IRI + if ($p->type === 'IRI') { + $quad .= "<{$p->value}>"; + } else if ($bnode !== null) { + // FIXME: TBD what to do with bnode predicates during normalization + // bnode normalization mode + $quad .= '_:p'; + } else { + // bnode normal mode + $quad .= $p->value; + } + $quad .= ' '; + + // object is IRI, bnode, or literal + if ($o->type === 'IRI') { + $quad .= "<{$o->value}>"; + } else if ($o->type === 'blank node') { + if ($bnode !== null) { + // normalization mode + $quad .= ($o->value === $bnode) ? '_:a' : '_:z'; + } else { + // normal mode + $quad .= $o->value; + } + } else { + $escaped = str_replace( + ['\\', "\t", "\n", "\r", '"'], ['\\\\', '\t', '\n', '\r', '\"'], $o->value); + $quad .= '"' . $escaped . '"'; + if ($o->datatype === self::RDF_LANGSTRING) { + if ($o->language) { + $quad .= "@{$o->language}"; + } + } else if ($o->datatype !== self::XSD_STRING) { + $quad .= "^^<{$o->datatype}>"; + } + } + + // graph + if ($g !== null) { + if (strpos($g, '_:') !== 0) { + $quad .= " <$g>"; + } else if ($bnode) { + $quad .= ' _:g'; + } else { + $quad .= " $g"; + } + } + + $quad .= " .\n"; + return $quad; + } + + /** + * Registers a processor-specific RDF dataset parser by content-type. + * Global parsers will no longer be used by this processor. + * + * @param string $content_type the content-type for the parser. + * @param callable $parser(input) the parser function (takes a string as + * a parameter and returns an RDF dataset). + */ + public function registerRDFParser($content_type, $parser) + { + if ($this->rdfParsers === null) { + $this->rdfParsers = new stdClass(); + } + $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 + * parsers will be re-enabled. + * + * @param string $content_type the content-type for the parser. + */ + public function unregisterRDFParser($content_type) + { + if ($this->rdfParsers !== null && + property_exists($this->rdfParsers, $content_type)) { + unset($this->rdfParsers->{$content_type}); + if (count(get_object_vars($content_type)) === 0) { + $this->rdfParsers = null; + } + } + } + + /** + * If $value is an array, returns $value, otherwise returns an array + * containing $value as the only element. + * + * @param mixed $value the value. + * + * @return array an array. + */ + public static function arrayify($value) + { + return is_array($value) ? $value : [$value]; + } + + /** + * Clones an object, array, or string/number. + * + * @param mixed $value the value to clone. + * + * @return mixed the cloned value. + */ + public static function copy($value) + { + if (is_object($value) || is_array($value)) { + return unserialize(serialize($value)); + } + return $value; + } + + /** + * 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 mixed $value the value to set. + */ + 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. + * @param assoc $defaults the default keys and values. + */ + 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. + * @param mixed $active_property the compacted property with the element + * to compact, null for none. + * @param mixed $element the element to compact. + * @param assoc $options the compaction options. + * + * @return mixed the compacted value. + */ + protected function _compact( + $active_ctx, $active_property, $element, $options) + { + // recursively compact array + if (is_array($element)) { + $rval = []; + foreach ($element as $e) { + // compact, dropping any null values + $compacted = $this->_compact( + $active_ctx, $active_property, $e, $options); + if ($compacted !== null) { + $rval[] = $compacted; + } + } + if ($options['compactArrays'] && count($rval) === 1) { + // use single element if no container is specified + $container = self::getContextValue( + $active_ctx, $active_property, '@container'); + if ($container === null) { + $rval = $rval[0]; + } + } + return $rval; + } + + // recursively compact object + if (is_object($element)) { + if ($options['link'] && property_exists($element, '@id') && + isset($options['link'][$element->{'@id'}])) { + // 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 + if (!isset($options['link'][$element->{'@id'}])) { + $options['link'][$element->{'@id'}] = []; + } + $options['link'][$element->{'@id'}][] = [ + 'expanded' => $element, 'compacted' => $rval]; + } + return $rval; + } + + // FIXME: avoid misuse of active property as an expanded property? + $inside_reverse = ($active_property === '@reverse'); + + $rval = new stdClass(); + + if ($options['link'] && property_exists($element, '@id')) { + // store linked element + if (!isset($options['link'][$element->{'@id'}])) { + $options['link'][$element->{'@id'}] = []; + } + $options['link'][$element->{'@id'}][] = [ + 'expanded' => $element, 'compacted' => $rval]; + } + + // process element keys in order + $keys = array_keys((array) $element); + sort($keys); + foreach ($keys as $expanded_property) { + $expanded_value = $element->{$expanded_property}; + + // compact @id and @type(s) + if ($expanded_property === '@id' || $expanded_property === '@type') { + if (is_string($expanded_value)) { + // compact single @id + $compacted_value = $this->_compactIri( + $active_ctx, $expanded_value, null, ['vocab' => ($expanded_property === '@type')]); + } else { + // expanded value must be a @type array + $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); + self::addValue( + $rval, $alias, $compacted_value, ['propertyIsArray' => $is_array]); + continue; + } + + // handle @reverse + if ($expanded_property === '@reverse') { + // recursively compact expanded value + $compacted_value = $this->_compact( + $active_ctx, '@reverse', $expanded_value, $options); + + // handle double-reversed properties + foreach ($compacted_value as $compacted_property => $value) { + if (property_exists($active_ctx->mappings, $compacted_property) && + $active_ctx->mappings->{$compacted_property} && + $active_ctx->mappings->{$compacted_property}->reverse) { + $container = self::getContextValue( + $active_ctx, $compacted_property, '@container'); + $use_array = ($container === '@set' || + !$options['compactArrays']); + self::addValue( + $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 + $alias = $this->_compactIri($active_ctx, $expanded_property); + self::addValue($rval, $alias, $compacted_value); + } + + continue; + } + + // handle @index property + if ($expanded_property === '@index') { + // drop @index if inside an @index container + $container = self::getContextValue( + $active_ctx, $active_property, '@container'); + if ($container === '@index') { + continue; + } + + // use keyword alias and add value + $alias = $this->_compactIri($active_ctx, $expanded_property); + 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' && + self::_isKeyword($expanded_property)) { + // use keyword alias and add value as is + $alias = $this->_compactIri($active_ctx, $expanded_property); + self::addValue($rval, $alias, $expanded_value); + continue; + } + + // Note: expanded value must be an array due to expansion algorithm. + // preserve empty arrays + if (count($expanded_value) === 0) { + $item_active_property = $this->_compactIri( + $active_ctx, $expanded_property, $expanded_value, ['vocab' => true], $inside_reverse); + self::addValue( + $rval, $item_active_property, [], ['propertyIsArray' => true]); + } + + // recusively process array values + foreach ($expanded_value as $expanded_item) { + // compact property and get container type + $item_active_property = $this->_compactIri( + $active_ctx, $expanded_property, $expanded_item, ['vocab' => true], $inside_reverse); + $container = self::getContextValue( + $active_ctx, $item_active_property, '@container'); + + // get @list value if appropriate + $is_list = self::_isList($expanded_item); + $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); + + // handle @list + if ($is_list) { + // ensure @list value is an array + $compacted_item = self::arrayify($compacted_item); + + if ($container !== '@list') { + // wrap using @list alias + $compacted_item = (object) [ + $this->_compactIri($active_ctx, '@list') => $compacted_item]; + + // include @index from expanded @list, if any + if (property_exists($expanded_item, '@index')) { + $compacted_item->{$this->_compactIri($active_ctx, '@index')} = $expanded_item->{'@index'}; + } + } else if (property_exists($rval, $item_active_property)) { + // can't use @list container for more than 1 list + 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 ' . + 'unwanted items into the list.', 'jsonld.SyntaxError', 'compaction to list of lists'); + } + } + + // handle language and index maps + if ($container === '@language' || $container === '@index') { + // get or create the map object + if (property_exists($rval, $item_active_property)) { + $map_object = $rval->{$item_active_property}; + } else { + $rval->{$item_active_property} = $map_object = new stdClass(); + } + + // if container is a language map, simplify compacted value to + // a simple string + if ($container === '@language' && self::_isValue($compacted_item)) { + $compacted_item = $compacted_item->{'@value'}; + } + + // add compact value to map object using key from expanded value + // based on the container type + self::addValue( + $map_object, $expanded_item->{$container}, $compacted_item); + } else { + // use an array if: compactArrays flag is false, + // @container is @set or @list, value is an empty + // array, or key is @graph + $is_array = (!$options['compactArrays'] || + $container === '@set' || $container === '@list' || + (is_array($compacted_item) && count($compacted_item) === 0) || + $expanded_property === '@list' || + $expanded_property === '@graph'); + + // add compact value + self::addValue( + $rval, $item_active_property, $compacted_item, ['propertyIsArray' => $is_array]); + } + } + } + + return $rval; + } + + // only primitives remain which are already compact + return $element; + } + + /** + * Recursively expands an element using the given context. Any context in + * the element will be removed. All context URLs must have been retrieved + * before calling this method. + * + * @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. + * @param assoc $options the expansion options. + * @param bool $inside_list true if the property is a list, false if not. + * + * @return mixed the expanded value. + */ + protected function _expand( + $active_ctx, $active_property, $element, $options, $inside_list) + { + // nothing to expand + if ($element === null) { + return $element; + } + + // recursively expand array + if (is_array($element)) { + $rval = []; + $container = self::getContextValue( + $active_ctx, $active_property, '@container'); + $inside_list = $inside_list || $container === '@list'; + foreach ($element as $e) { + // expand element + $e = $this->_expand( + $active_ctx, $active_property, $e, $options, $inside_list); + if ($inside_list && (is_array($e) || self::_isList($e))) { + // lists of lists are illegal + throw new JsonLdException( + 'Invalid JSON-LD syntax; lists of lists are not permitted.', 'jsonld.SyntaxError', 'list of lists'); + } + // drop null values + if ($e !== null) { + if (is_array($e)) { + $rval = array_merge($rval, $e); + } else { + $rval[] = $e; + } + } + } + return $rval; + } + + if (!is_object($element)) { + // drop free-floating scalars that are not in lists + if (!$inside_list && + ($active_property === null || + $this->_expandIri($active_ctx, $active_property, ['vocab' => true]) === '@graph')) { + return null; + } + + // expand element according to value expansion rules + return $this->_expandValue($active_ctx, $active_property, $element); + } + + // recursively expand object: + // if element has a context, process it + if (property_exists($element, '@context')) { + $active_ctx = $this->_processContext( + $active_ctx, $element->{'@context'}, $options); + } + + // expand the active property + $expanded_active_property = $this->_expandIri( + $active_ctx, $active_property, ['vocab' => true]); + + $rval = new stdClass(); + $keys = array_keys((array) $element); + sort($keys); + foreach ($keys as $key) { + $value = $element->{$key}; + + if ($key === '@context') { + continue; + } + + // expand key to IRI + $expanded_property = $this->_expandIri( + $active_ctx, $key, ['vocab' => true]); + + // drop non-absolute IRI keys that aren't keywords + if ($expanded_property === null || + !(self::_isAbsoluteIri($expanded_property) || + self::_isKeyword($expanded_property))) { + continue; + } + + 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 ' . + 'property.', 'jsonld.SyntaxError', 'invalid reverse property map', ['value' => $value]); + } + if (property_exists($rval, $expanded_property)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; colliding keywords detected.', 'jsonld.SyntaxError', 'colliding keywords', ['keyword' => $expanded_property]); + } + } + + // syntax error if @id is not a string + if ($expanded_property === '@id' && !is_string($value)) { + if (!isset($options['isFrame']) || !$options['isFrame']) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; "@id" value must a string.', 'jsonld.SyntaxError', 'invalid @id value', ['value' => $value]); + } + if (!is_object($value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; "@id" value must a string or an object.', 'jsonld.SyntaxError', 'invalid @id value', ['value' => $value]); + } + } + + // validate @type value + if ($expanded_property === '@type') { + $this->_validateTypeValue($value); + } + + // @graph must be an array or an object + if ($expanded_property === '@graph' && + !(is_object($value) || is_array($value))) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; "@graph" value must not be an ' . + 'object or an array.', 'jsonld.SyntaxError', 'invalid @graph value', ['value' => $value]); + } + + // @value must not be an object or an array + if ($expanded_property === '@value' && + (is_object($value) || is_array($value))) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; "@value" value must not be an ' . + 'object or an array.', 'jsonld.SyntaxError', 'invalid value object value', ['value' => $value]); + } + + // @language must be a string + if ($expanded_property === '@language') { + if ($value === null) { + // drop null @language values, they expand as if they didn't exist + continue; + } + 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]); + } + // ensure language value is lowercase + $value = strtolower($value); + } + + // @index must be a string + if ($expanded_property === '@index') { + if (!is_string($value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; "@index" value must be a string.', 'jsonld.SyntaxError', 'invalid @index value', ['value' => $value]); + } + } + + // @reverse must be an object + if ($expanded_property === '@reverse') { + if (!is_object($value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; "@reverse" value must be an object.', 'jsonld.SyntaxError', 'invalid @reverse value', ['value' => $value]); + } + + $expanded_value = $this->_expand( + $active_ctx, '@reverse', $value, $options, $inside_list); + + // properties double-reversed + if (property_exists($expanded_value, '@reverse')) { + foreach ($expanded_value->{'@reverse'} as $rproperty => $rvalue) { + self::addValue( + $rval, $rproperty, $rvalue, ['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'}; + } else { + $reverse_map = null; + } + foreach ($expanded_value as $property => $items) { + if ($property === '@reverse') { + continue; + } + if ($reverse_map === null) { + $reverse_map = $rval->{'@reverse'} = new stdClass(); + } + self::addValue( + $reverse_map, $property, [], ['propertyIsArray' => true]); + foreach ($items as $item) { + 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]); + } + self::addValue( + $reverse_map, $property, $item, ['propertyIsArray' => true]); + } + } + + continue; + } + + $container = self::getContextValue($active_ctx, $key, '@container'); + + if ($container === '@language' && is_object($value)) { + // handle language map container (skip if value is not an object) + $expanded_value = $this->_expandLanguageMap($value); + } else if ($container === '@index' && is_object($value)) { + // handle index container (skip if value is not an object) + $expanded_value = []; + $value_keys = array_keys((array) $value); + sort($value_keys); + foreach ($value_keys as $value_key) { + $val = $value->{$value_key}; + $val = self::arrayify($val); + $val = $this->_expand($active_ctx, $key, $val, $options, false); + foreach ($val as $item) { + if (!property_exists($item, '@index')) { + $item->{'@index'} = $value_key; + } + $expanded_value[] = $item; + } + } + } else { + // recurse into @list or @set + $is_list = ($expanded_property === '@list'); + if ($is_list || $expanded_property === '@set') { + $next_active_property = $active_property; + if ($is_list && $expanded_active_property === '@graph') { + $next_active_property = null; + } + $expanded_value = $this->_expand( + $active_ctx, $next_active_property, $value, $options, $is_list); + if ($is_list && self::_isList($expanded_value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; lists of lists are not permitted.', 'jsonld.SyntaxError', 'list of lists'); + } + } else { + // recursively expand value with key as new active property + $expanded_value = $this->_expand( + $active_ctx, $key, $value, $options, false); + } + } + + // drop null values if property is not @value + if ($expanded_value === null && $expanded_property !== '@value') { + continue; + } + + // convert expanded value to @list if container specifies it + if ($expanded_property !== '@list' && !self::_isList($expanded_value) && + $container === '@list') { + // ensure expanded value is an array + $expanded_value = (object) [ + '@list' => self::arrayify($expanded_value)]; + } + + // FIXME: can this be merged with code above to simplify? + // merge in reverse properties + if (property_exists($active_ctx->mappings, $key) && + $active_ctx->mappings->{$key} && + $active_ctx->mappings->{$key}->reverse) { + if (property_exists($rval, '@reverse')) { + $reverse_map = $rval->{'@reverse'}; + } else { + $reverse_map = $rval->{'@reverse'} = new stdClass(); + } + $expanded_value = self::arrayify($expanded_value); + foreach ($expanded_value as $item) { + 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]); + } + self::addValue( + $reverse_map, $expanded_property, $item, ['propertyIsArray' => true]); + } + continue; + } + + // add value for property + // use an array except for certain keywords + $use_array = (!in_array( + $expanded_property, [ + '@index', '@id', '@type', '@value', '@language'])); + self::addValue( + $rval, $expanded_property, $expanded_value, ['propertyIsArray' => $use_array]); + } + + // get property count on expanded output + $keys = array_keys((array) $rval); + $count = count($keys); + + // @value must only have @language or @type + if (property_exists($rval, '@value')) { + // @value must only have @language or @type + if (property_exists($rval, '@type') && + property_exists($rval, '@language')) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; an element containing "@value" may not ' . + 'contain both "@type" and "@language".', 'jsonld.SyntaxError', 'invalid value object', ['element' => $rval]); + } + $valid_count = $count - 1; + if (property_exists($rval, '@type')) { + $valid_count -= 1; + } + if (property_exists($rval, '@index')) { + $valid_count -= 1; + } + if (property_exists($rval, '@language')) { + $valid_count -= 1; + } + if ($valid_count !== 0) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; an element containing "@value" may only ' . + 'have an "@index" property and at most one other property ' . + 'which can be "@type" or "@language".', 'jsonld.SyntaxError', 'invalid value object', ['element' => $rval]); + } + // drop null @values + if ($rval->{'@value'} === null) { + $rval = null; + } else if (property_exists($rval, '@language') && + !is_string($rval->{'@value'})) { + // if @language is present, @value must be a string + throw new JsonLdException( + 'Invalid JSON-LD syntax; only strings may be language-tagged.', 'jsonld.SyntaxError', 'invalid language-tagged value', ['element' => $rval]); + } else if (property_exists($rval, '@type') && + (!self::_isAbsoluteIri($rval->{'@type'}) || + strpos($rval->{'@type'}, '_:') === 0)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; an element containing "@value" ' . + 'and "@type" must have an absolute IRI for the value ' . + 'of "@type".', 'jsonld.SyntaxError', 'invalid typed value', ['element' => $rval]); + } + } else if (property_exists($rval, '@type') && !is_array($rval->{'@type'})) { + // convert @type to an array + $rval->{'@type'} = [$rval->{'@type'}]; + } else if (property_exists($rval, '@set') || + property_exists($rval, '@list')) { + // handle @set and @list + if ($count > 1 && !($count === 2 && property_exists($rval, '@index'))) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; if an element has the property "@set" ' . + 'or "@list", then it can have at most one other property that is ' . + '"@index".', 'jsonld.SyntaxError', 'invalid set or list object', ['element' => $rval]); + } + // optimize away @set + if (property_exists($rval, '@set')) { + $rval = $rval->{'@set'}; + $keys = array_keys((array) $rval); + $count = count($keys); + } + } else if ($count === 1 && property_exists($rval, '@language')) { + // drop objects with only @language + $rval = null; + } + + // drop certain top-level objects that do not occur in lists + if (is_object($rval) && + !$options['keepFreeFloatingNodes'] && !$inside_list && + ($active_property === null || $expanded_active_property === '@graph')) { + // drop empty object or top-level @value/@list, or object with only @id + if ($count === 0 || property_exists($rval, '@value') || + property_exists($rval, '@list') || + ($count === 1 && property_exists($rval, '@id'))) { + $rval = null; + } + } + + return $rval; + } + + /** + * Performs JSON-LD flattening. + * + * @param array $input the expanded JSON-LD to flatten. + * + * @return array the flattened output. + */ + protected function _flatten($input) + { + // produce a map of all subjects and name each bnode + $namer = new UniqueNamer('_:b'); + $graphs = (object) ['@default' => new stdClass()]; + $this->_createNodeMap($input, $graphs, '@default', $namer); + + // add all non-default graphs to default graph + $default_graph = $graphs->{'@default'}; + $graph_names = array_keys((array) $graphs); + foreach ($graph_names as $graph_name) { + if ($graph_name === '@default') { + continue; + } + $node_map = $graphs->{$graph_name}; + if (!property_exists($default_graph, $graph_name)) { + $default_graph->{$graph_name} = (object) [ + '@id' => $graph_name, '@graph' => []]; + } + $subject = $default_graph->{$graph_name}; + if (!property_exists($subject, '@graph')) { + $subject->{'@graph'} = []; + } + $ids = array_keys((array) $node_map); + sort($ids); + foreach ($ids as $id) { + $node = $node_map->{$id}; + // only add full subjects + if (!self::_isSubjectReference($node)) { + $subject->{'@graph'}[] = $node; + } + } + } + + // produce flattened output + $flattened = []; + $keys = array_keys((array) $default_graph); + sort($keys); + foreach ($keys as $key) { + $node = $default_graph->{$key}; + // only add full subjects to top-level + if (!self::_isSubjectReference($node)) { + $flattened[] = $node; + } + } + return $flattened; + } + + /** + * Performs JSON-LD framing. + * + * @param array $input the expanded JSON-LD to frame. + * @param array $frame the expanded JSON-LD frame to use. + * @param assoc $options the framing options. + * + * @return array the framed output. + */ + protected function _frame($input, $frame, $options) + { + // create framing state + $state = (object) [ + 'options' => $options, + 'graphs' => (object) [ + '@default' => new stdClass(), + '@merged' => new stdClass()], + 'subjectStack' => [], + 'link' => new stdClass()]; + + // produce a map of all graphs and name each bnode + // FIXME: currently uses subjects from @merged graph only + $namer = new UniqueNamer('_:b'); + $this->_createNodeMap($input, $state->graphs, '@merged', $namer); + $state->subjects = $state->graphs->{'@merged'}; + + // frame the subjects + $framed = new ArrayObject(); + $keys = array_keys((array) $state->subjects); + sort($keys); + $this->_matchFrame($state, $keys, $frame, $framed, null); + return (array) $framed; + } + + /** + * Performs normalization on the given RDF dataset. + * + * @param stdClass $dataset the RDF dataset to normalize. + * @param assoc $options the normalization options. + * + * @return mixed the normalized output. + */ + protected function _normalize($dataset, $options) + { + // create quads and map bnodes to their associated quads + $quads = []; + $bnodes = new stdClass(); + foreach ($dataset as $graph_name => $triples) { + if ($graph_name === '@default') { + $graph_name = null; + } + foreach ($triples as $triple) { + $quad = $triple; + if ($graph_name !== null) { + if (strpos($graph_name, '_:') === 0) { + $quad->name = (object) [ + 'type' => 'blank node', 'value' => $graph_name]; + } else { + $quad->name = (object) [ + 'type' => 'IRI', 'value' => $graph_name]; + } + } + $quads[] = $quad; + + foreach (['subject', 'object', 'name'] as $attr) { + if (property_exists($quad, $attr) && + $quad->{$attr}->type === 'blank node') { + $id = $quad->{$attr}->value; + if (property_exists($bnodes, $id)) { + $bnodes->{$id}->quads[] = $quad; + } else { + $bnodes->{$id} = (object) ['quads' => [$quad]]; + } + } + } + } + } + + // mapping complete, start canonical naming + $namer = new UniqueNamer('_:c14n'); + + // continue to hash bnode quads while bnodes are assigned names + $unnamed = null; + $nextUnnamed = array_keys((array) $bnodes); + $duplicates = null; + do { + $unnamed = $nextUnnamed; + $nextUnnamed = []; + $duplicates = new stdClass(); + $unique = new stdClass(); + foreach ($unnamed as $bnode) { + // hash quads for each unnamed bnode + $hash = $this->_hashQuads($bnode, $bnodes, $namer); + + // store hash as unique or a duplicate + if (property_exists($duplicates, $hash)) { + $duplicates->{$hash}[] = $bnode; + $nextUnnamed[] = $bnode; + } else if (property_exists($unique, $hash)) { + $duplicates->{$hash} = [$unique->{$hash}, $bnode]; + $nextUnnamed[] = $unique->{$hash}; + $nextUnnamed[] = $bnode; + unset($unique->{$hash}); + } else { + $unique->{$hash} = $bnode; + } + } + + // name unique bnodes in sorted hash order + $hashes = array_keys((array) $unique); + sort($hashes); + foreach ($hashes as $hash) { + $namer->getName($unique->{$hash}); + } + } while (count($unnamed) > count($nextUnnamed)); + + // enumerate duplicate hash groups in sorted order + $hashes = array_keys((array) $duplicates); + sort($hashes); + foreach ($hashes as $hash) { + // process group + $group = $duplicates->{$hash}; + $results = []; + foreach ($group as $bnode) { + // skip already-named bnodes + if ($namer->isNamed($bnode)) { + continue; + } + + // hash bnode paths + $path_namer = new UniqueNamer('_:b'); + $path_namer->getName($bnode); + $results[] = $this->_hashPaths($bnode, $bnodes, $namer, $path_namer); + } + + // name bnodes in hash order + usort($results, function($a, $b) { + $a = $a->hash; + $b = $b->hash; + return ($a < $b) ? -1 : (($a > $b) ? 1 : 0); + }); + foreach ($results as $result) { + // name all bnodes in path namer in key-entry order + foreach ($result->pathNamer->order as $bnode) { + $namer->getName($bnode); + } + } + } + + // create normalized array + $normalized = []; + + /* Note: At this point all bnodes in the set of RDF quads have been + assigned canonical names, which have been stored in the 'namer' object. + Here each quad is updated by assigning each of its bnodes its new name + via the 'namer' object. */ + + // update bnode names in each quad and serialize + foreach ($quads as $quad) { + foreach (['subject', 'object', 'name'] as $attr) { + if (property_exists($quad, $attr) && + $quad->{$attr}->type === 'blank node' && + strpos($quad->{$attr}->value, '_:c14n') !== 0) { + $quad->{$attr}->value = $namer->getName($quad->{$attr}->value); + } + } + $normalized[] = $this->toNQuad($quad, property_exists($quad, 'name') ? + $quad->name->value : null); + } + + // sort normalized output + sort($normalized); + + // handle output format + if (isset($options['format']) && $options['format']) { + if ($options['format'] === 'application/nquads') { + return implode($normalized); + } + throw new JsonLdException( + 'Unknown output format.', 'jsonld.UnknownFormat', null, ['format' => $options['format']]); + } + + // return RDF dataset + return $this->parseNQuads(implode($normalized)); + } + + /** + * Converts an RDF dataset to JSON-LD. + * + * @param stdClass $dataset the RDF dataset. + * @param assoc $options the RDF serialization options. + * + * @return array the JSON-LD output. + */ + protected function _fromRDF($dataset, $options) + { + $default_graph = new stdClass(); + $graph_map = (object) ['@default' => $default_graph]; + $referenced_once = (object) []; + + foreach ($dataset as $name => $graph) { + if (!property_exists($graph_map, $name)) { + $graph_map->{$name} = new stdClass(); + } + if ($name !== '@default' && !property_exists($default_graph, $name)) { + $default_graph->{$name} = (object) ['@id' => $name]; + } + $node_map = $graph_map->{$name}; + foreach ($graph as $triple) { + // get subject, predicate, object + $s = $triple->subject->value; + $p = $triple->predicate->value; + $o = $triple->object; + + if (!property_exists($node_map, $s)) { + $node_map->{$s} = (object) ['@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)) { + $node_map->{$o->value} = (object) ['@id' => $o->value]; + } + + if ($p === self::RDF_TYPE && !$options['useRdfType'] && $object_is_id) { + self::addValue( + $node, '@type', $o->value, ['propertyIsArray' => true]); + continue; + } + + $value = self::_RDFToObject($o, $options['useNativeTypes']); + self::addValue($node, $p, $value, ['propertyIsArray' => true]); + + // object may be an RDF list/partial list node but we can't know + // easily until all triples are read + if ($object_is_id) { + if ($o->value === self::RDF_NIL) { + $object = $node_map->{$o->value}; + if (!property_exists($object, 'usages')) { + $object->usages = []; + } + $object->usages[] = (object) [ + 'node' => $node, + 'property' => $p, + 'value' => $value]; + } else if (property_exists($referenced_once, $o->value)) { + // object referenced more than once + $referenced_once->{$o->value} = false; + } else { + // track single reference + $referenced_once->{$o->value} = (object) [ + 'node' => $node, + 'property' => $p, + 'value' => $value]; + } + } + } + } + + // convert linked lists to @list arrays + foreach ($graph_map as $name => $graph_object) { + // no @lists to be converted, continue + if (!property_exists($graph_object, self::RDF_NIL)) { + continue; + } + + // iterate backwards through each RDF list + $nil = $graph_object->{self::RDF_NIL}; + foreach ($nil->usages as $usage) { + $node = $usage->node; + $property = $usage->property; + $head = $usage->value; + $list = []; + $list_nodes = []; + + // ensure node is a well-formed list node; it must: + // 1. Be referenced only once. + // 2. Have an array for rdf:first that has 1 item. + // 3. Have an array for rdf:rest that has 1 item. + // 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)); + while ($property === self::RDF_REST && + property_exists($referenced_once, $node->{'@id'}) && + is_object($referenced_once->{$node->{'@id'}}) && + property_exists($node, self::RDF_FIRST) && + property_exists($node, self::RDF_REST) && + is_array($node->{self::RDF_FIRST}) && + is_array($node->{self::RDF_REST}) && + count($node->{self::RDF_FIRST}) === 1 && + count($node->{self::RDF_REST}) === 1 && + ($node_key_count === 3 || ($node_key_count === 4 && + 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'}; + + // get next node, moving backwards through list + $usage = $referenced_once->{$node->{'@id'}}; + $node = $usage->node; + $property = $usage->property; + $head = $usage->value; + $node_key_count = count(array_keys((array) $node)); + + // if node is not a blank node, then list head found + if (strpos($node->{'@id'}, '_:') !== 0) { + break; + } + } + + // list is nested in another list + if ($property === self::RDF_FIRST) { + // 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 + continue; + } + + // preserve list head + $head = $graph_object->{$head->{'@id'}}->{self::RDF_REST}[0]; + array_pop($list); + array_pop($list_nodes); + } + + // transform list into @list object + unset($head->{'@id'}); + $head->{'@list'} = array_reverse($list); + foreach ($list_nodes as $list_node) { + unset($graph_object->{$list_node}); + } + } + + unset($nil->usages); + } + + $result = []; + $subjects = array_keys((array) $default_graph); + sort($subjects); + foreach ($subjects as $subject) { + $node = $default_graph->{$subject}; + if (property_exists($graph_map, $subject)) { + $node->{'@graph'} = []; + $graph_object = $graph_map->{$subject}; + $subjects_ = array_keys((array) $graph_object); + sort($subjects_); + foreach ($subjects_ as $subject_) { + $node_ = $graph_object->{$subject_}; + // only add full subjects to top-level + if (!self::_isSubjectReference($node_)) { + $node->{'@graph'}[] = $node_; + } + } + } + // only add full subjects to top-level + if (!self::_isSubjectReference($node)) { + $result[] = $node; + } + } + + return $result; + } + + /** + * Processes a local context and returns a new active context. + * + * @param stdClass $active_ctx the current active context. + * @param mixed $local_ctx the local context to process. + * @param assoc $options the context processing options. + * + * @return stdClass the new active context. + */ + protected function _processContext($active_ctx, $local_ctx, $options) + { + global $jsonld_cache; + + // normalize local context to an array + if (is_object($local_ctx) && property_exists($local_ctx, '@context') && + is_array($local_ctx->{'@context'})) { + $local_ctx = $local_ctx->{'@context'}; + } + $ctxs = self::arrayify($local_ctx); + + // no contexts in array, clone existing context + if (count($ctxs) === 0) { + return self::_cloneActiveContext($active_ctx); + } + + // process each context in order, update active context + // on each iteration to ensure proper caching + $rval = $active_ctx; + foreach ($ctxs as $ctx) { + // reset to initial context + if ($ctx === null) { + $rval = $active_ctx = $this->_getInitialContext($options); + continue; + } + + // dereference @context key if present + if (is_object($ctx) && property_exists($ctx, '@context')) { + $ctx = $ctx->{'@context'}; + } + + // context must be an object by now, all URLs retrieved before this call + if (!is_object($ctx)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context must be an object.', 'jsonld.SyntaxError', 'invalid local context', ['context' => $ctx]); + } + + // get context from cache if available + if (property_exists($jsonld_cache, 'activeCtx')) { + $cached = $jsonld_cache->activeCtx->get($active_ctx, $ctx); + if ($cached) { + $rval = $active_ctx = $cached; + $must_clone = true; + continue; + } + } + + // update active context and clone new one before updating + $active_ctx = $rval; + $rval = self::_cloneActiveContext($rval); + + // define context mappings for keys in local context + $defined = new stdClass(); + + // handle @base + if (property_exists($ctx, '@base')) { + $base = $ctx->{'@base'}; + if ($base === null) { + $base = null; + } else if (!is_string($base)) { + throw new JsonLdException( + '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)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; the value of "@base" in a ' . + '@context must be an absolute IRI or the empty string.', 'jsonld.SyntaxError', 'invalid base IRI', ['context' => $ctx]); + } + if ($base !== null) { + $base = jsonld_parse_url($base); + } + $rval->{'@base'} = $base; + $defined->{'@base'} = true; + } + + // handle @vocab + if (property_exists($ctx, '@vocab')) { + $value = $ctx->{'@vocab'}; + if ($value === null) { + unset($rval->{'@vocab'}); + } else if (!is_string($value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; the value of "@vocab" in a ' . + '@context must be a string or null.', 'jsonld.SyntaxError', 'invalid vocab mapping', ['context' => $ctx]); + } else if (!self::_isAbsoluteIri($value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; the value of "@vocab" in a ' . + '@context must be an absolute IRI.', 'jsonld.SyntaxError', 'invalid vocab mapping', ['context' => $ctx]); + } else { + $rval->{'@vocab'} = $value; + } + $defined->{'@vocab'} = true; + } + + // handle @language + if (property_exists($ctx, '@language')) { + $value = $ctx->{'@language'}; + if ($value === null) { + unset($rval->{'@language'}); + } else if (!is_string($value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; the value of "@language" in a ' . + '@context must be a string or null.', 'jsonld.SyntaxError', 'invalid default language', ['context' => $ctx]); + } else { + $rval->{'@language'} = strtolower($value); + } + $defined->{'@language'} = true; + } + + // process all other keys + foreach ($ctx as $k => $v) { + $this->_createTermDefinition($rval, $ctx, $k, $defined); + } + + // cache result + if (property_exists($jsonld_cache, 'activeCtx')) { + $jsonld_cache->activeCtx->set($active_ctx, $ctx, $rval); + } + } + + return $rval; + } + + /** + * Expands a language map. + * + * @param stdClass $language_map the language map to expand. + * + * @return array the expanded language map. + */ + protected function _expandLanguageMap($language_map) + { + $rval = []; + $keys = array_keys((array) $language_map); + sort($keys); + foreach ($keys as $key) { + $values = $language_map->{$key}; + $values = self::arrayify($values); + foreach ($values as $item) { + if ($item === null) { + continue; + } + if (!is_string($item)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; language map values must be strings.', 'jsonld.SyntaxError', 'invalid language map value', ['languageMap', $language_map]); + } + $rval[] = (object) [ + '@value' => $item, + '@language' => strtolower($key)]; + } + } + return $rval; + } + + /** + * Labels the blank nodes in the given value using the given UniqueNamer. + * + * @param UniqueNamer $namer the UniqueNamer to use. + * @param mixed $element the element with blank nodes to rename. + * + * @return mixed the element. + */ + public function _labelBlankNodes($namer, $element) + { + if (is_array($element)) { + $length = count($element); + for ($i = 0; $i < $length; ++$i) { + $element[$i] = $this->_labelBlankNodes($namer, $element[$i]); + } + } else if (self::_isList($element)) { + $element->{'@list'} = $this->_labelBlankNodes( + $namer, $element->{'@list'}); + } else if (is_object($element)) { + // rename blank node + if (self::_isBlankNode($element)) { + $name = null; + if (property_exists($element, '@id')) { + $name = $element->{'@id'}; + } + $element->{'@id'} = $namer->getName($name); + } + + // recursively apply to all keys + $keys = array_keys((array) $element); + sort($keys); + foreach ($keys as $key) { + if ($key !== '@id') { + $element->{$key} = $this->_labelBlankNodes($namer, $element->{$key}); + } + } + } + + return $element; + } + + /** + * Expands the given value by using the coercion and keyword rules in the + * given context. + * + * @param stdClass $active_ctx the active context to use. + * @param string $active_property the property the value is associated with. + * @param mixed $value the value to expand. + * + * @return mixed the expanded value. + */ + protected function _expandValue($active_ctx, $active_property, $value) + { + // nothing to expand + if ($value === null) { + return null; + } + + // special-case expand @id and @type (skips '@id' expansion) + $expanded_property = $this->_expandIri( + $active_ctx, $active_property, ['vocab' => true]); + if ($expanded_property === '@id') { + return $this->_expandIri($active_ctx, $value, ['base' => true]); + } else if ($expanded_property === '@type') { + return $this->_expandIri( + $active_ctx, $value, ['vocab' => true, 'base' => true]); + } + + // get type definition from context + $type = self::getContextValue($active_ctx, $active_property, '@type'); + + // do @id expansion (automatic for @graph) + if ($type === '@id' || ($expanded_property === '@graph' && + is_string($value))) { + return (object) ['@id' => $this->_expandIri( + $active_ctx, $value, ['base' => true])]; + } + // do @id expansion w/vocab + if ($type === '@vocab') { + return (object) ['@id' => $this->_expandIri( + $active_ctx, $value, ['vocab' => true, 'base' => true])]; + } + + // do not expand keyword values + if (self::_isKeyword($expanded_property)) { + return $value; + } + + $rval = new stdClass(); + + // other type + if ($type !== null) { + $rval->{'@type'} = $type; + } else if (is_string($value)) { + // check for language tagging for strings + $language = self::getContextValue( + $active_ctx, $active_property, '@language'); + if ($language !== null) { + $rval->{'@language'} = $language; + } + } + $rval->{'@value'} = $value; + + return $rval; + } + + /** + * Creates an array of RDF triples for the given graph. + * + * @param stdClass $graph the graph to create RDF triples for. + * @param UniqueNamer $namer for assigning bnode names. + * @param assoc $options the RDF serialization options. + * + * @return array the array of RDF triples for the given graph. + */ + protected function _graphToRDF($graph, $namer, $options) + { + $rval = []; + + $ids = array_keys((array) $graph); + sort($ids); + foreach ($ids as $id) { + $node = $graph->{$id}; + if ($id === '"') { + $id = ''; + } + $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)) { + continue; + } + + foreach ($items as $item) { + // skip relative IRI subjects and predicates + if (!(self::_isAbsoluteIri($id) && self::_isAbsoluteIri($property))) { + continue; + } + + // RDF subject + $subject = new stdClass(); + $subject->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; + $subject->value = $id; + + // RDF predicate + $predicate = new stdClass(); + $predicate->type = (strpos($property, '_:') === 0 ? + 'blank node' : 'IRI'); + $predicate->value = $property; + + // skip bnode predicates unless producing generalized RDF + if ($predicate->type === 'blank node' && + !$options['produceGeneralizedRdf']) { + continue; + } + + if (self::_isList($item)) { + // convert @list to triples + $this->_listToRDF( + $item->{'@list'}, $namer, $subject, $predicate, $rval); + } else { + // convert value or node object to triple + $object = $this->_objectToRDF($item); + // skip null objects (they are relative IRIs) + if ($object) { + $rval[] = (object) [ + 'subject' => $subject, + 'predicate' => $predicate, + 'object' => $object]; + } + } + } + } + } + + return $rval; + } + + /** + * Converts a @list value into linked list of blank node RDF triples + * (an RDF collection). + * + * @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. + * @param stdClass $predicate the predicate for the head of the list. + * @param &array $triples the array of triples to append to. + */ + protected function _listToRDF( + $list, $namer, $subject, $predicate, &$triples) + { + $first = (object) ['type' => 'IRI', 'value' => self::RDF_FIRST]; + $rest = (object) ['type' => 'IRI', 'value' => self::RDF_REST]; + $nil = (object) ['type' => 'IRI', 'value' => self::RDF_NIL]; + + foreach ($list as $item) { + $blank_node = (object) [ + 'type' => 'blank node', 'value' => $namer->getName()]; + $triples[] = (object) [ + 'subject' => $subject, + 'predicate' => $predicate, + 'object' => $blank_node]; + + $subject = $blank_node; + $predicate = $first; + $object = $this->_objectToRDF($item); + // skip null objects (they are relative IRIs) + if ($object) { + $triples[] = (object) [ + 'subject' => $subject, + 'predicate' => $predicate, + 'object' => $object]; + } + + $predicate = $rest; + } + + $triples[] = (object) [ + 'subject' => $subject, 'predicate' => $predicate, 'object' => $nil]; + } + + /** + * Converts a JSON-LD value object to an RDF literal or a JSON-LD string or + * node object to an RDF resource. + * + * @param mixed $item the JSON-LD value or node object. + * + * @return stdClass the RDF literal or RDF resource. + */ + protected function _objectToRDF($item) + { + $object = new stdClass(); + + if (self::_isValue($item)) { + $object->type = 'literal'; + $value = $item->{'@value'}; + $datatype = property_exists($item, '@type') ? $item->{'@type'} : null; + + // convert to XSD datatypes as appropriate + if (is_bool($value)) { + $object->value = ($value ? 'true' : 'false'); + $object->datatype = $datatype ? $datatype : self::XSD_BOOLEAN; + } else if (is_double($value) || $datatype == self::XSD_DOUBLE) { + // canonical double representation + $object->value = preg_replace( + '/(\d)0*E\+?/', '$1E', sprintf('%1.15E', $value)); + $object->datatype = $datatype ? $datatype : self::XSD_DOUBLE; + } else if (is_integer($value)) { + $object->value = strval($value); + $object->datatype = $datatype ? $datatype : self::XSD_INTEGER; + } else if (property_exists($item, '@language')) { + $object->value = $value; + $object->datatype = $datatype ? $datatype : self::RDF_LANGSTRING; + $object->language = $item->{'@language'}; + } else { + $object->value = $value; + $object->datatype = $datatype ? $datatype : self::XSD_STRING; + } + } else { + // convert string/node object to RDF + $id = is_object($item) ? $item->{'@id'} : $item; + $object->type = (strpos($id, '_:') === 0) ? 'blank node' : 'IRI'; + $object->value = $id; + } + + // skip relative IRIs + if ($object->type === 'IRI' && !self::_isAbsoluteIri($object->value)) { + return null; + } + + return $object; + } + + /** + * Converts an RDF triple object to a JSON-LD object. + * + * @param stdClass $o the RDF triple object to convert. + * @param bool $use_native_types true to output native types, false not to. + * + * @return stdClass the JSON-LD object. + */ + protected function _RDFToObject($o, $use_native_types) + { + // convert IRI/blank node object to JSON-LD + if ($o->type === 'IRI' || $o->type === 'blank node') { + return (object) ['@id' => $o->value]; + } + + // convert literal object to JSON-LD + $rval = (object) ['@value' => $o->value]; + + if (property_exists($o, 'language')) { + // add language + $rval->{'@language'} = $o->language; + } else { + // add datatype + $type = $o->datatype; + // use native types for certain xsd types + if ($use_native_types) { + if ($type === self::XSD_BOOLEAN) { + if ($rval->{'@value'} === 'true') { + $rval->{'@value'} = true; + } else if ($rval->{'@value'} === 'false') { + $rval->{'@value'} = false; + } + } else if (is_numeric($rval->{'@value'})) { + if ($type === self::XSD_INTEGER) { + $i = intval($rval->{'@value'}); + if (strval($i) === $rval->{'@value'}) { + $rval->{'@value'} = $i; + } + } else if ($type === self::XSD_DOUBLE) { + $rval->{'@value'} = doubleval($rval->{'@value'}); + } + } + // do not add native type + if (!in_array($type, [ + self::XSD_BOOLEAN, self::XSD_INTEGER, self::XSD_DOUBLE, + self::XSD_STRING])) { + $rval->{'@type'} = $type; + } + } else if ($type !== self::XSD_STRING) { + $rval->{'@type'} = $type; + } + } + + return $rval; + } + + /** + * Recursively flattens the subjects in the given JSON-LD expanded input + * into a node map. + * + * @param mixed $input the JSON-LD expanded input. + * @param stdClass $graphs a map of graph name to subject map. + * @param string $graph the name of the current graph. + * @param UniqueNamer $namer the blank node namer. + * @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) + { + // recurse through array + if (is_array($input)) { + foreach ($input as $e) { + $this->_createNodeMap($e, $graphs, $graph, $namer, null, $list); + } + return; + } + + // add non-object to list + if (!is_object($input)) { + if ($list !== null) { + $list[] = $input; + } + return; + } + + // add values to list + if (self::_isValue($input)) { + if (property_exists($input, '@type')) { + $type = $input->{'@type'}; + // rename @type blank node + if (strpos($type, '_:') === 0) { + $type = $input->{'@type'} = $namer->getName($type); + } + } + if ($list !== null) { + $list[] = $input; + } + return; + } + + // Note: At this point, input must be a subject. + // spec requires @type to be named first, so assign names early + if (property_exists($input, '@type')) { + foreach ($input->{'@type'} as $type) { + if (strpos($type, '_:') === 0) { + $namer->getName($type); + } + } + } + + // get name for subject + if ($name === null) { + if (property_exists($input, '@id')) { + $name = $input->{'@id'}; + } + if (self::_isBlankNode($input)) { + $name = $namer->getName($name); + } + } + + // add subject reference to list + if ($list !== null) { + $list[] = (object) ['@id' => $name]; + } + + // create new subject or merge into existing one + if (!property_exists($graphs, $graph)) { + $graphs->{$graph} = new stdClass(); + } + $subjects = $graphs->{$graph}; + if (!property_exists($subjects, $name)) { + if ($name === '') { + $subjects->{'"'} = new stdClass(); + } else { + $subjects->{$name} = new stdClass(); + } + } + if ($name === '') { + $subject = $subjects->{'"'}; + } else { + $subject = $subjects->{$name}; + } + $subject->{'@id'} = $name; + $properties = array_keys((array) $input); + sort($properties); + foreach ($properties as $property) { + // skip @id + if ($property === '@id') { + continue; + } + + // handle reverse properties + if ($property === '@reverse') { + $referenced_node = (object) ['@id' => $name]; + $reverse_map = $input->{'@reverse'}; + foreach ($reverse_map as $reverse_property => $items) { + foreach ($items as $item) { + $item_name = null; + if (property_exists($item, '@id')) { + $item_name = $item->{'@id'}; + } + if (self::_isBlankNode($item)) { + $item_name = $namer->getName($item_name); + } + $this->_createNodeMap($item, $graphs, $graph, $namer, $item_name); + if ($item_name === '') { + $item_name = '"'; + } + self::addValue( + $subjects->{$item_name}, $reverse_property, $referenced_node, ['propertyIsArray' => true, 'allowDuplicate' => false]); + } + } + continue; + } + + // recurse into graph + if ($property === '@graph') { + // add graph subjects map entry + if (!property_exists($graphs, $name)) { + // FIXME: temporary hack to avoid empty property bug + if (!$name) { + $name = '"'; + } + $graphs->{$name} = new stdClass(); + } + $g = ($graph === '@merged') ? $graph : $name; + $this->_createNodeMap( + $input->{$property}, $graphs, $g, $namer, null, null); + continue; + } + + // copy non-@type keywords + if ($property !== '@type' && self::_isKeyword($property)) { + if ($property === '@index' && property_exists($subject, '@index') && + ($input->{'@index'} !== $subject->{'@index'} || + $input->{'@index'}->{'@id'} !== $subject->{'@index'}->{'@id'})) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; conflicting @index property detected.', 'jsonld.SyntaxError', 'conflicting indexes', ['subject' => $subject]); + } + $subject->{$property} = $input->{$property}; + continue; + } + + // iterate over objects + $objects = $input->{$property}; + + // if property is a bnode, assign it a new id + if (strpos($property, '_:') === 0) { + $property = $namer->getName($property); + } + + // ensure property is added for empty arrays + if (count($objects) === 0) { + self::addValue( + $subject, $property, [], ['propertyIsArray' => true]); + continue; + } + foreach ($objects as $o) { + if ($property === '@type') { + // rename @type blank nodes + $o = (strpos($o, '_:') === 0) ? $namer->getName($o) : $o; + } + + // handle embedded subject or subject reference + if (self::_isSubject($o) || self::_isSubjectReference($o)) { + // rename blank node @id + $id = property_exists($o, '@id') ? $o->{'@id'} : null; + if (self::_isBlankNode($o)) { + $id = $namer->getName($id); + } + + // add reference and recurse + self::addValue( + $subject, $property, (object) ['@id' => $id], ['propertyIsArray' => true, 'allowDuplicate' => false]); + $this->_createNodeMap($o, $graphs, $graph, $namer, $id, null); + } else if (self::_isList($o)) { + // handle @list + $_list = new ArrayObject(); + $this->_createNodeMap( + $o->{'@list'}, $graphs, $graph, $namer, $name, $_list); + $o = (object) ['@list' => (array) $_list]; + self::addValue( + $subject, $property, $o, ['propertyIsArray' => true, 'allowDuplicate' => false]); + } else { + // handle @value + $this->_createNodeMap($o, $graphs, $graph, $namer, $name, null); + self::addValue( + $subject, $property, $o, ['propertyIsArray' => true, 'allowDuplicate' => false]); + } + } + } + } + + /** + * Frames subjects according to the given frame. + * + * @param stdClass $state the current framing state. + * @param array $subjects the subjects to filter. + * @param array $frame the frame. + * @param mixed $parent the parent subject or top-level array. + * @param mixed $property the parent property, initialized to null. + */ + protected function _matchFrame( + $state, $subjects, $frame, $parent, $property) + { + // validate the frame + $this->_validateFrame($frame); + $frame = $frame[0]; + + // get flags for current frame + $options = $state->options; + $flags = [ + 'embed' => $this->_getFrameFlag($frame, $options, 'embed'), + 'explicit' => $this->_getFrameFlag($frame, $options, 'explicit'), + 'requireAll' => $this->_getFrameFlag($frame, $options, 'requireAll')]; + + // filter out subjects that match the frame + $matches = $this->_filterSubjects($state, $subjects, $frame, $flags); + + // add matches to output + foreach ($matches as $id => $subject) { + if ($flags['embed'] === '@link' && property_exists($state->link, $id)) { + // TODO: may want to also match an existing linked subject against + // the current frame ... so different frames could produce different + // subjects that are only shared in-memory when the frames are the same + // add existing linked subject + $this->_addFrameOutput($parent, $property, $state->link->{$id}); + 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, + which only occurs at the top-level. */ + if ($property === null) { + $state->uniqueEmbeds = new stdClass(); + } + + // 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 + // embed, the subject cannot be embedded, just add the reference; + // note that a circular reference won't occur when the embed flag is + // `@link` as the above check will short-circuit before reaching this point + if ($flags['embed'] === '@never' || + $this->_createsCircularReference($subject, $state->subjectStack)) { + $this->_addFrameOutput($parent, $property, $output); + continue; + } + + // if only the last match should be embedded + if ($flags['embed'] === '@last') { + // remove any existing embed + if (property_exists($state->uniqueEmbeds, $id)) { + $this->_removeEmbed($state, $id); + } + $state->uniqueEmbeds->{$id} = [ + 'parent' => $parent, 'property' => $property]; + } + + // push matching subject onto stack to enable circular embed checks + $state->subjectStack[] = $subject; + + // iterate over subject properties + $props = array_keys((array) $subject); + sort($props); + foreach ($props as $prop) { + // copy keywords to output + if (self::_isKeyword($prop)) { + $output->{$prop} = self::copy($subject->{$prop}); + continue; + } + + // explicit is on and property isn't in the frame, skip processing + if ($flags['explicit'] && !property_exists($frame, $prop)) { + continue; + } + + // add objects + $objects = $subject->{$prop}; + foreach ($objects as $o) { + // recurse into list + if (self::_isList($o)) { + // add empty list + $list = (object) ['@list' => []]; + $this->_addFrameOutput($output, $prop, $list); + + // add list objects + $src = $o->{'@list'}; + foreach ($src as $o) { + if (self::_isSubjectReference($o)) { + // recurse into subject reference + $subframe = (property_exists($frame, $prop) ? + $frame->{$prop}[0]->{'@list'} : + $this->_createImplicitFrame($flags)); + $this->_matchFrame( + $state, [$o->{'@id'}], $subframe, $list, '@list'); + } else { + // include other values automatically + $this->_addFrameOutput($list, '@list', self::copy($o)); + } + } + continue; + } + + if (self::_isSubjectReference($o)) { + // recurse into subject reference + $subframe = (property_exists($frame, $prop) ? + $frame->{$prop} : $this->_createImplicitFrame($flags)); + $this->_matchFrame( + $state, [$o->{'@id'}], $subframe, $output, $prop); + } else { + // include other values automatically + $this->_addFrameOutput($output, $prop, self::copy($o)); + } + } + } + + // handle defaults + $props = array_keys((array) $frame); + sort($props); + foreach ($props as $prop) { + // skip keywords + if (self::_isKeyword($prop)) { + continue; + } + + // if omit default is off, then include default values for properties + // that appear in the next frame but are not in the matching subject + $next = $frame->{$prop}[0]; + $omit_default_on = $this->_getFrameFlag( + $next, $options, 'omitDefault'); + if (!$omit_default_on && !property_exists($output, $prop)) { + $preserve = '@null'; + if (property_exists($next, '@default')) { + $preserve = self::copy($next->{'@default'}); + } + $preserve = self::arrayify($preserve); + $output->{$prop} = [(object) ['@preserve' => $preserve]]; + } + } + + // add output to parent + $this->_addFrameOutput($parent, $property, $output); + + // pop matching subject from circular ref-checking stack + array_pop($state->subjectStack); + } + } + + /** + * 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 + * parent frame used. + * + * @param assoc flags the current framing flags. + * + * @return array the implicit frame. + */ + function _createImplicitFrame($flags) + { + $frame = new stdClass(); + foreach ($flags as $key => $value) { + $frame->{'@' . $key} = [$flags[$key]]; + } + return [$frame]; + } + + /** + * Checks the current subject stack to see if embedding the given subject + * would cause a circular reference. + * + * @param stdClass subject_to_embed the subject to embed. + * @param assoc subject_stack the current stack of subjects. + * + * @return bool true if a circular reference would be created, false if not. + */ + function _createsCircularReference($subject_to_embed, $subject_stack) + { + for ($i = count($subject_stack) - 1; $i >= 0; --$i) { + if ($subject_stack[$i]->{'@id'} === $subject_to_embed->{'@id'}) { + return true; + } + } + return false; + } + + /** + * Gets the frame flag value for the given flag name. + * + * @param stdClass $frame the frame. + * @param stdClass $options the framing options. + * @param string $name the flag name. + * + * @return mixed $the flag value. + */ + protected function _getFrameFlag($frame, $options, $name) + { + $flag = "@$name"; + $rval = (property_exists($frame, $flag) ? + $frame->{$flag}[0] : $options[$name]); + if ($name === 'embed') { + // default is "@last" + // backwards-compatibility support for "embed" maps: + // true => "@last" + // false => "@never" + if ($rval === true) { + $rval = '@last'; + } else if ($rval === false) { + $rval = '@never'; + } else if ($rval !== '@always' && $rval !== '@never' && + $rval !== '@link') { + $rval = '@last'; + } + } + return $rval; + } + + /** + * Validates a JSON-LD frame, throwing an exception if the frame is invalid. + * + * @param array $frame the frame to validate. + */ + protected function _validateFrame($frame) + { + if (!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.', 'jsonld.SyntaxError', null, ['frame' => $frame]); + } + } + + /** + * Returns a map of all of the subjects that match a parsed frame. + * + * @param stdClass $state the current framing state. + * @param array $subjects the set of subjects to filter. + * @param stdClass $frame the parsed frame. + * @param assoc $flags the frame flags. + * + * @return stdClass all of the matched subjects. + */ + protected function _filterSubjects($state, $subjects, $frame, $flags) + { + $rval = new stdClass(); + sort($subjects); + foreach ($subjects as $id) { + $subject = $state->subjects->{$id}; + if ($this->_filterSubject($subject, $frame, $flags)) { + $rval->{$id} = $subject; + } + } + return $rval; + } + + /** + * Returns true if the given subject matches the given frame. + * + * @param stdClass $subject the subject to check. + * @param stdClass $frame the frame to check. + * @param assoc $flags the frame flags. + * + * @return bool true if the subject matches, false if not. + */ + protected function _filterSubject($subject, $frame, $flags) + { + // check @type (object value means 'any' type, fall through to ducktyping) + if (property_exists($frame, '@type') && + !(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) { + $types = $frame->{'@type'}; + foreach ($types as $type) { + // any matching @type is a match + if (self::hasValue($subject, '@type', $type)) { + return true; + } + } + return false; + } + + // check ducktype + $wildcard = true; + $matches_some = false; + foreach ($frame as $k => $v) { + if (self::_isKeyword($k)) { + // skip non-@id and non-@type + if ($k !== '@id' && $k !== '@type') { + continue; + } + $wildcard = false; + + // check @id for a specific @id value + if ($k === '@id' && is_string($v)) { + if (!property_exists($subject, $k) || $subject->{$k} !== $v) { + return false; + } + $matches_some = true; + continue; + } + } + + $wildcard = false; + + if (property_exists($subject, $k)) { + // $v === [] means do not match if property is present + if (is_array($v) && count($v) === 0) { + return false; + } + $matches_some = true; + continue; + } + + // all properties must match to be a duck unless a @default is specified + $has_default = (is_array($v) && count($v) === 1 && is_object($v[0]) && + property_exists($v[0], '@default')); + if ($flags['requireAll'] && !$has_default) { + return false; + } + } + + // return true if wildcard or subject matches some properties + return $wildcard || $matches_some; + } + + /** + * Removes an existing embed. + * + * @param stdClass $state the current framing state. + * @param string $id the @id of the embed to remove. + */ + protected function _removeEmbed($state, $id) + { + // get existing embed + $embeds = $state->uniqueEmbeds; + $embed = $embeds->{$id}; + $property = $embed['property']; + + // create reference to replace embed + $subject = (object) ['@id' => $id]; + + // remove existing embed + if (is_array($embed->parent)) { + // replace subject with reference + foreach ($embed->parent as $i => $parent) { + if (self::compareValues($parent, $subject)) { + $embed->parent[$i] = $subject; + break; + } + } + } else { + // replace subject with reference + $use_array = is_array($embed->parent->{$property}); + self::removeValue($embed->parent, $property, $subject, ['propertyIsArray' => $use_array]); + self::addValue($embed->parent, $property, $subject, ['propertyIsArray' => $use_array]); + } + + // recursively remove dependent dangling embeds + $removeDependents = function($id) { + // get embed keys as a separate array to enable deleting keys in map + $ids = array_keys((array) $embeds); + foreach ($ids as $next) { + if (property_exists($embeds, $next) && + is_object($embeds->{$next}->parent) && + $embeds->{$next}->parent->{'@id'} === $id) { + unset($embeds->{$next}); + $removeDependents($next); + } + } + }; + $removeDependents($id); + } + + /** + * Adds framing output to the given parent. + * + * @param mixed $parent the parent to add to. + * @param string $property the parent property. + * @param mixed $output the output to add. + */ + protected function _addFrameOutput($parent, $property, $output) + { + if (is_object($parent) && !($parent instanceof ArrayObject)) { + self::addValue( + $parent, $property, $output, ['propertyIsArray' => true]); + } else { + $parent[] = $output; + } + } + + /** + * Removes the @preserve keywords as the last step of the framing algorithm. + * + * @param stdClass $ctx the active context used to compact the input. + * @param mixed $input the framed, compacted output. + * @param assoc $options the compaction options used. + * + * @return mixed the resulting output. + */ + protected function _removePreserve($ctx, $input, $options) + { + // recurse through arrays + if (is_array($input)) { + $output = []; + foreach ($input as $e) { + $result = $this->_removePreserve($ctx, $e, $options); + // drop nulls from arrays + if ($result !== null) { + $output[] = $result; + } + } + $input = $output; + } else if (is_object($input)) { + // remove @preserve + if (property_exists($input, '@preserve')) { + if ($input->{'@preserve'} === '@null') { + return null; + } + return $input->{'@preserve'}; + } + + // skip @values + if (self::_isValue($input)) { + return $input; + } + + // recurse through @lists + if (self::_isList($input)) { + $input->{'@list'} = $this->_removePreserve( + $ctx, $input->{'@list'}, $options); + return $input; + } + + // handle in-memory linked nodes + $id_alias = $this->_compactIri($ctx, '@id'); + if (property_exists($input, $id_alias)) { + $id = $input->{$id_alias}; + if (isset($options['link'][$id])) { + $idx = array_search($input, $options['link'][$id]); + if ($idx === false) { + // prevent circular visitation + $options['link'][$id][] = $input; + } else { + // already visited + return $options['link'][$id][$idx]; + } + } else { + // prevent circular visitation + $options['link'][$id] = [$input]; + } + } + + // recurse through properties + foreach ($input as $prop => $v) { + $result = $this->_removePreserve($ctx, $v, $options); + $container = self::getContextValue($ctx, $prop, '@container'); + if ($options['compactArrays'] && + is_array($result) && count($result) === 1 && + $container !== '@set' && $container !== '@list') { + $result = $result[0]; + } + $input->{$prop} = $result; + } + } + return $input; + } + + /** + * Compares two RDF triples for equality. + * + * @param stdClass $t1 the first triple. + * @param stdClass $t2 the second triple. + * + * @return true if the triples are the same, false if not. + */ + protected static function _compareRDFTriples($t1, $t2) + { + foreach (['subject', 'predicate', 'object'] as $attr) { + if ($t1->{$attr}->type !== $t2->{$attr}->type || + $t1->{$attr}->value !== $t2->{$attr}->value) { + return false; + } + } + if (property_exists($t1->object, 'language') !== property_exists($t1->object, 'language')) { + return false; + } + if (property_exists($t1->object, 'language') && + $t1->object->language !== $t2->object->language) { + return false; + } + if (property_exists($t1->object, 'datatype') && + $t1->object->datatype !== $t2->object->datatype) { + return false; + } + return true; + } + + /** + * Hashes all of the quads about a blank node. + * + * @param string $id the ID of the bnode to hash quads for. + * @param stdClass $bnodes the mapping of bnodes to quads. + * @param UniqueNamer $namer the canonical bnode namer. + * + * @return string the new hash. + */ + protected function _hashQuads($id, $bnodes, $namer) + { + // return cached hash + if (property_exists($bnodes->{$id}, 'hash')) { + return $bnodes->{$id}->hash; + } + + // serialize all of bnode's quads + $quads = $bnodes->{$id}->quads; + $nquads = []; + foreach ($quads as $quad) { + $nquads[] = $this->toNQuad($quad, property_exists($quad, 'name') ? + $quad->name->value : null, $id); + } + + // sort serialized quads + sort($nquads); + + // cache and return hashed quads + $hash = $bnodes->{$id}->hash = sha1(implode($nquads)); + return $hash; + } + + /** + * 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 + * lexicographically-least 'path' serializations. + * + * @param string $id the ID of the bnode to hash paths for. + * @param stdClass $bnodes the map of bnode quads. + * @param UniqueNamer $namer the canonical bnode namer. + * @param UniqueNamer $path_namer the namer used to assign names to adjacent + * bnodes. + * + * @return stdClass the hash and path namer used. + */ + protected function _hashPaths($id, $bnodes, $namer, $path_namer) + { + // create SHA-1 digest + $md = hash_init('sha1'); + + // group adjacent bnodes by hash, keep properties and references separate + $groups = new stdClass(); + $quads = $bnodes->{$id}->quads; + foreach ($quads as $quad) { + // get adjacent bnode + $bnode = $this->_getAdjacentBlankNodeName($quad->subject, $id); + if ($bnode !== null) { + // normal property + $direction = 'p'; + } else { + $bnode = $this->_getAdjacentBlankNodeName($quad->object, $id); + if ($bnode !== null) { + // reverse property + $direction = 'r'; + } + } + if ($bnode !== null) { + // get bnode name (try canonical, path, then hash) + if ($namer->isNamed($bnode)) { + $name = $namer->getName($bnode); + } else if ($path_namer->isNamed($bnode)) { + $name = $path_namer->getName($bnode); + } else { + $name = $this->_hashQuads($bnode, $bnodes, $namer); + } + + // hash direction, property, and bnode name/hash + $group_md = hash_init('sha1'); + hash_update($group_md, $direction); + hash_update($group_md, $quad->predicate->value); + hash_update($group_md, $name); + $group_hash = hash_final($group_md); + + // add bnode to hash group + if (property_exists($groups, $group_hash)) { + $groups->{$group_hash}[] = $bnode; + } else { + $groups->{$group_hash} = [$bnode]; + } + } + } + + // iterate over groups in sorted hash order + $group_hashes = array_keys((array) $groups); + sort($group_hashes); + foreach ($group_hashes as $group_hash) { + // digest group hash + hash_update($md, $group_hash); + + // choose a path and namer from the permutations + $chosen_path = null; + $chosen_namer = null; + $permutator = new Permutator($groups->{$group_hash}); + while ($permutator->hasNext()) { + $permutation = $permutator->next(); + $path_namer_copy = clone $path_namer; + + // build adjacent path + $path = ''; + $skipped = false; + $recurse = []; + foreach ($permutation as $bnode) { + // use canonical name if available + if ($namer->isNamed($bnode)) { + $path .= $namer->getName($bnode); + } else { + // recurse if bnode isn't named in the path yet + if (!$path_namer_copy->isNamed($bnode)) { + $recurse[] = $bnode; + } + $path .= $path_namer_copy->getName($bnode); + } + + // skip permutation if path is already >= chosen path + if ($chosen_path !== null && strlen($path) >= strlen($chosen_path) && + $path > $chosen_path) { + $skipped = true; + break; + } + } + + // recurse + if (!$skipped) { + foreach ($recurse as $bnode) { + $result = $this->_hashPaths( + $bnode, $bnodes, $namer, $path_namer_copy); + $path .= $path_namer_copy->getName($bnode); + $path .= "<{$result->hash}>"; + $path_namer_copy = $result->pathNamer; + + // skip permutation if path is already >= chosen path + if ($chosen_path !== null && + strlen($path) >= strlen($chosen_path) && $path > $chosen_path) { + $skipped = true; + break; + } + } + } + + if (!$skipped && ($chosen_path === null || $path < $chosen_path)) { + $chosen_path = $path; + $chosen_namer = $path_namer_copy; + } + } + + // digest chosen path and update namer + hash_update($md, $chosen_path); + $path_namer = $chosen_namer; + } + + // return SHA-1 hash and path namer + return (object) [ + 'hash' => hash_final($md), 'pathNamer' => $path_namer]; + } + + /** + * 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 + * 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. + * + * @return mixed the adjacent blank node name or null if none was found. + */ + protected function _getAdjacentBlankNodeName($node, $id) + { + if ($node->type === 'blank node' && $node->value !== $id) { + return $node->value; + } + return null; + } + + /** + * Compares two strings first based on length and then lexicographically. + * + * @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. + */ + protected function _compareShortestLeast($a, $b) + { + $len_a = strlen($a); + $len_b = strlen($b); + if ($len_a < $len_b) { + return -1; + } + if ($len_b < $len_a) { + return 1; + } + if ($a === $b) { + return 0; + } + return ($a < $b) ? -1 : 1; + } + + /** + * Picks the preferred compaction term from the given inverse context entry. + * + * @param active_ctx the active context. + * @param iri the IRI to pick the term for. + * @param value the value to pick the term for. + * @param containers the preferred containers. + * @param type_or_language either '@type' or '@language'. + * @param type_or_language_value the preferred value for '@type' or + * '@language'. + * + * @return mixed the preferred term. + */ + protected function _selectTerm( + $active_ctx, $iri, $value, $containers, $type_or_language, $type_or_language_value) + { + if ($type_or_language_value === null) { + $type_or_language_value = '@null'; + } + + // options for the value of @type or @language + $prefs = []; + + // determine prefs for @id based on whether or not value compacts to a term + if (($type_or_language_value === '@id' || + $type_or_language_value === '@reverse') && + self::_isSubjectReference($value)) { + // prefer @reverse first + if ($type_or_language_value === '@reverse') { + $prefs[] = '@reverse'; + } + // try to compact value to a term + $term = $this->_compactIri( + $active_ctx, $value->{'@id'}, null, ['vocab' => true]); + if (property_exists($active_ctx->mappings, $term) && + $active_ctx->mappings->{$term} && + $active_ctx->mappings->{$term}->{'@id'} === $value->{'@id'}) { + // prefer @vocab + array_push($prefs, '@vocab', '@id'); + } else { + // prefer @id + array_push($prefs, '@id', '@vocab'); + } + } else { + $prefs[] = $type_or_language_value; + } + $prefs[] = '@none'; + + $container_map = $active_ctx->inverse->{$iri}; + foreach ($containers as $container) { + // if container not available in the map, continue + if (!property_exists($container_map, $container)) { + continue; + } + + $type_or_language_value_map = $container_map->{$container}->{$type_or_language}; + foreach ($prefs as $pref) { + // if type/language option not available in the map, continue + if (!property_exists($type_or_language_value_map, $pref)) { + continue; + } + + // select term + return $type_or_language_value_map->{$pref}; + } + } + return null; + } + + /** + * 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. + * + * @param stdClass $active_ctx the active context to use. + * @param string $iri the IRI to compact. + * @param mixed $value the value to check or null. + * @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 + * if not. + * + * @return string the compacted term, prefix, keyword alias, or original IRI. + */ + protected function _compactIri( + $active_ctx, $iri, $value = null, $relative_to = [], $reverse = false) + { + // can't compact null + if ($iri === null) { + return $iri; + } + + $inverse_ctx = $this->_getInverseContext($active_ctx); + + if (self::_isKeyword($iri)) { + // a keyword can only be compacted to simple alias + if (property_exists($inverse_ctx, $iri)) { + return $inverse_ctx->$iri->{'@none'}->{'@type'}->{'@none'}; + } + return $iri; + } + + if (!isset($relative_to['vocab'])) { + $relative_to['vocab'] = false; + } + + // use inverse context to pick a term if iri is relative to vocab + if ($relative_to['vocab'] && property_exists($inverse_ctx, $iri)) { + $default_language = '@none'; + if (property_exists($active_ctx, '@language')) { + $default_language = $active_ctx->{'@language'}; + } + + // prefer @index if available in value + $containers = []; + if (is_object($value) && property_exists($value, '@index')) { + $containers[] = '@index'; + } + + // defaults for term selection based on type/language + $type_or_language = '@language'; + $type_or_language_value = '@null'; + + if ($reverse) { + $type_or_language = '@type'; + $type_or_language_value = '@reverse'; + $containers[] = '@set'; + } 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 (!property_exists($value, '@index')) { + $containers[] = '@list'; + } + $list = $value->{'@list'}; + $common_language = (count($list) === 0) ? $default_language : null; + $common_type = null; + foreach ($list as $item) { + $item_language = '@none'; + $item_type = '@none'; + if (self::_isValue($item)) { + if (property_exists($item, '@language')) { + $item_language = $item->{'@language'}; + } else if (property_exists($item, '@type')) { + $item_type = $item->{'@type'}; + } else { + // plain literal + $item_language = '@null'; + } + } else { + $item_type = '@id'; + } + if ($common_language === null) { + $common_language = $item_language; + } else if ($item_language !== $common_language && + self::_isValue($item)) { + $common_language = '@none'; + } + if ($common_type === null) { + $common_type = $item_type; + } else if ($item_type !== $common_type) { + $common_type = '@none'; + } + // there are different languages and types in the list, so choose + // the most generic term, no need to keep iterating the list + if ($common_language === '@none' && $common_type === '@none') { + break; + } + } + if ($common_language === null) { + $common_language = '@none'; + } + if ($common_type === null) { + $common_type = '@none'; + } + if ($common_type !== '@none') { + $type_or_language = '@type'; + $type_or_language_value = $common_type; + } else { + $type_or_language_value = $common_language; + } + } else { + if (self::_isValue($value)) { + if (property_exists($value, '@language') && + !property_exists($value, '@index')) { + $containers[] = '@language'; + $type_or_language_value = $value->{'@language'}; + } else if (property_exists($value, '@type')) { + $type_or_language = '@type'; + $type_or_language_value = $value->{'@type'}; + } + } else { + $type_or_language = '@type'; + $type_or_language_value = '@id'; + } + $containers[] = '@set'; + } + + // do term selection + $containers[] = '@none'; + $term = $this->_selectTerm( + $active_ctx, $iri, $value, $containers, $type_or_language, $type_or_language_value); + if ($term !== null) { + return $term; + } + } + + // no term match, use @vocab if available + if ($relative_to['vocab']) { + if (property_exists($active_ctx, '@vocab')) { + // determine if vocab is a prefix of the iri + $vocab = $active_ctx->{'@vocab'}; + if (strpos($iri, $vocab) === 0 && $iri !== $vocab) { + // use suffix as relative iri if it is not a term in the active + // context + $suffix = substr($iri, strlen($vocab)); + if (!property_exists($active_ctx->mappings, $suffix)) { + return $suffix; + } + } + } + } + + // no term or @vocab match, check for possible CURIEs + $choice = null; + $idx = 0; + $partial_matches = []; + $iri_map = $active_ctx->fast_curie_map; + // check for partial matches of against `iri`, which means look until + // iri.length - 1, not full length + $max_partial_length = strlen($iri) - 1; + for (; $idx < $max_partial_length && isset($iri_map[$iri[$idx]]); ++$idx) { + $iri_map = $iri_map[$iri[$idx]]; + if (isset($iri_map[''])) { + $entry = $iri_map[''][0]; + $entry->iri_length = $idx + 1; + $partial_matches[] = $entry; + } + } + // check partial matches in reverse order to prefer longest ones first + $partial_matches = array_reverse($partial_matches); + foreach ($partial_matches as $entry) { + $terms = $entry->terms; + 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 + // the mapping matches the IRI + $curie = $term . ':' . substr($iri, $entry->iri_length); + $is_usable_curie = (!property_exists($active_ctx->mappings, $curie) || + ($value === null && + $active_ctx->mappings->{$curie}->{'@id'} === $iri)); + + // select curie if it is shorter or the same length but + // lexicographically less than the current choice + if ($is_usable_curie && ($choice === null || + self::_compareShortestLeast($curie, $choice) < 0)) { + $choice = $curie; + } + } + } + + // return chosen curie + if ($choice !== null) { + return $choice; + } + + // compact IRI relative to base + if (!$relative_to['vocab']) { + return jsonld_remove_base($active_ctx->{'@base'}, $iri); + } + + // return IRI as is + return $iri; + } + + /** + * Performs value compaction on an object with '@value' or '@id' as the only + * property. + * + * @param stdClass $active_ctx the active context. + * @param string $active_property the active property that points to the + * value. + * @param mixed $value the value to compact. + * + * @return mixed the compaction result. + */ + protected function _compactValue($active_ctx, $active_property, $value) + { + // value is a @value + if (self::_isValue($value)) { + // get context rules + $type = self::getContextValue($active_ctx, $active_property, '@type'); + $language = self::getContextValue( + $active_ctx, $active_property, '@language'); + $container = self::getContextValue( + $active_ctx, $active_property, '@container'); + + // whether or not the value has an @index that must be preserved + $preserve_index = (property_exists($value, '@index') && + $container !== '@index'); + + // if there's no @index to preserve + if (!$preserve_index) { + // matching @type or @language specified in context, compact value + if (self::_hasKeyValue($value, '@type', $type) || + self::_hasKeyValue($value, '@language', $language)) { + return $value->{'@value'}; + } + } + + // return just the value of @value if all are true: + // 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 + $key_count = count(array_keys((array) $value)); + $is_value_only_key = ($key_count === 1 || + ($key_count === 2 && property_exists($value, '@index') && + !$preserve_index)); + $has_default_language = property_exists($active_ctx, '@language'); + $is_value_string = is_string($value->{'@value'}); + $has_null_mapping = ( + property_exists($active_ctx->mappings, $active_property) && + $active_ctx->mappings->{$active_property} !== null && + self::_hasKeyValue( + $active_ctx->mappings->{$active_property}, '@language', null)); + if ($is_value_only_key && + (!$has_default_language || !$is_value_string || $has_null_mapping)) { + return $value->{'@value'}; + } + + $rval = new stdClass(); + + // preserve @index + if ($preserve_index) { + $rval->{$this->_compactIri($active_ctx, '@index')} = $value->{'@index'}; + } + + // compact @type IRI + if (property_exists($value, '@type')) { + $rval->{$this->_compactIri($active_ctx, '@type')} = $this->_compactIri( + $active_ctx, $value->{'@type'}, null, ['vocab' => true]); + } else if (property_exists($value, '@language')) { + // alias @language + $rval->{$this->_compactIri($active_ctx, '@language')} = $value->{'@language'}; + } + + // alias @value + $rval->{$this->_compactIri($active_ctx, '@value')} = $value->{'@value'}; + + return $rval; + } + + // value is a subject reference + $expanded_property = $this->_expandIri( + $active_ctx, $active_property, ['vocab' => true]); + $type = self::getContextValue($active_ctx, $active_property, '@type'); + $compacted = $this->_compactIri( + $active_ctx, $value->{'@id'}, null, ['vocab' => ($type === '@vocab')]); + + // compact to scalar + if ($type === '@id' || $type === '@vocab' || + $expanded_property === '@graph') { + return $compacted; + } + + $rval = (object) [ + $this->_compactIri($active_ctx, '@id') => $compacted]; + return $rval; + } + + /** + * Creates a term definition during context processing. + * + * @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. + * @param stdClass $defined a map of defining/defined keys to detect cycles + * and prevent double definitions. + */ + protected function _createTermDefinition( + $active_ctx, $local_ctx, $term, $defined) + { + if (property_exists($defined, $term)) { + // term already defined + if ($defined->{$term}) { + return; + } + // cycle detected + throw new JsonLdException( + 'Cyclical context definition detected.', 'jsonld.CyclicalContext', 'cyclic IRI mapping', ['context' => $local_ctx, 'term' => $term]); + } + + // now defining term + $defined->{$term} = false; + + if (self::_isKeyword($term)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; keywords cannot be overridden.', 'jsonld.SyntaxError', 'keyword redefinition', ['context' => $local_ctx, 'term' => $term]); + } + + // remove old mapping + if (property_exists($active_ctx->mappings, $term)) { + unset($active_ctx->mappings->{$term}); + } + + // get context term value + $value = $local_ctx->{$term}; + + // clear context entry + if ($value === null || (is_object($value) && + self::_hasKeyValue($value, '@id', null))) { + $active_ctx->mappings->{$term} = null; + $defined->{$term} = true; + return; + } + + // convert short-hand value to object w/@id + if (is_string($value)) { + $value = (object) ['@id' => $value]; + } + + if (!is_object($value)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context property values must be ' . + 'strings or objects.', 'jsonld.SyntaxError', 'invalid term definition', ['context' => $local_ctx]); + } + + // create new mapping + $mapping = $active_ctx->mappings->{$term} = new stdClass(); + $mapping->reverse = false; + + if (property_exists($value, '@reverse')) { + if (property_exists($value, '@id')) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; a @reverse term definition must not ' + + 'contain @id.', 'jsonld.SyntaxError', 'invalid reverse property', ['context' => $local_ctx]); + } + $reverse = $value->{'@reverse'}; + if (!is_string($reverse)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; a @context @reverse value must be a string.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]); + } + + // expand and add @id mapping + $id = $this->_expandIri( + $active_ctx, $reverse, ['vocab' => true, 'base' => false], $local_ctx, $defined); + if (!self::_isAbsoluteIri($id)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context @reverse value must be ' . + 'an absolute IRI or a blank node identifier.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]); + } + $mapping->{'@id'} = $id; + $mapping->reverse = true; + } else if (property_exists($value, '@id')) { + $id = $value->{'@id'}; + if (!is_string($id)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context @id value must be a string.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]); + } + if ($id !== $term) { + // add @id to mapping + $id = $this->_expandIri( + $active_ctx, $id, ['vocab' => true, 'base' => false], $local_ctx, $defined); + if (!self::_isAbsoluteIri($id) && !self::_isKeyword($id)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context @id value must be an ' . + 'absolute IRI, a blank node identifier, or a keyword.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx]); + } + $mapping->{'@id'} = $id; + } + } + + // always compute whether term has a colon as an optimization for + // _compactIri + $colon = strpos($term, ':'); + $mapping->_term_has_colon = ($colon !== false); + + if (!property_exists($mapping, '@id')) { + // see if the term has a prefix + if ($mapping->_term_has_colon) { + $prefix = substr($term, 0, $colon); + if (property_exists($local_ctx, $prefix)) { + // define parent prefix + $this->_createTermDefinition( + $active_ctx, $local_ctx, $prefix, $defined); + } + + if (property_exists($active_ctx->mappings, $prefix) && + $active_ctx->mappings->{$prefix}) { + // set @id based on prefix parent + $suffix = substr($term, $colon + 1); + $mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} . + $suffix; + } else { + // term is an absolute IRI + $mapping->{'@id'} = $term; + } + } else { + // non-IRIs *must* define @ids if @vocab is not available + if (!property_exists($active_ctx, '@vocab')) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context terms must define an @id.', 'jsonld.SyntaxError', 'invalid IRI mapping', ['context' => $local_ctx, 'term' => $term]); + } + // prepend vocab to term + $mapping->{'@id'} = $active_ctx->{'@vocab'} . $term; + } + } + + // optimization to store length of @id once for _compactIri + $mapping->_id_length = strlen($mapping->{'@id'}); + + // IRI mapping now defined + $defined->{$term} = true; + + if (property_exists($value, '@type')) { + $type = $value->{'@type'}; + if (!is_string($type)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context @type values must be strings.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]); + } + + if ($type !== '@id' && $type !== '@vocab') { + // expand @type to full IRI + $type = $this->_expandIri( + $active_ctx, $type, ['vocab' => true], $local_ctx, $defined); + if (!self::_isAbsoluteIri($type)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; an @context @type value must ' . + 'be an absolute IRI.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]); + } + if (strpos($type, '_:') === 0) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; an @context @type values must ' . + 'be an IRI, not a blank node identifier.', 'jsonld.SyntaxError', 'invalid type mapping', ['context' => $local_ctx]); + } + } + + // add @type to mapping + $mapping->{'@type'} = $type; + } + + if (property_exists($value, '@container')) { + $container = $value->{'@container'}; + if ($container !== '@list' && $container !== '@set' && + $container !== '@index' && $container !== '@language') { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context @container value must be ' . + 'one of the following: @list, @set, @index, or @language.', 'jsonld.SyntaxError', 'invalid container mapping', ['context' => $local_ctx]); + } + if ($mapping->reverse && $container !== '@index' && + $container !== '@set' && $container !== null) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context @container value for a @reverse ' + + 'type definition must be @index or @set.', 'jsonld.SyntaxError', 'invalid reverse property', ['context' => $local_ctx]); + } + + // add @container to mapping + $mapping->{'@container'} = $container; + } + + if (property_exists($value, '@language') && + !property_exists($value, '@type')) { + $language = $value->{'@language'}; + if ($language !== null && !is_string($language)) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; @context @language value must be ' . + 'a string or null.', 'jsonld.SyntaxError', 'invalid language mapping', ['context' => $local_ctx]); + } + + // add @language to mapping + if ($language !== null) { + $language = strtolower($language); + } + $mapping->{'@language'} = $language; + } + + // disallow aliasing @context and @preserve + $id = $mapping->{'@id'}; + 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]); + } + } + + /** + * Expands a string to a full IRI. The string may be a term, a prefix, a + * relative IRI, or an absolute IRI. The associated absolute IRI will be + * returned. + * + * @param stdClass $active_ctx the current active context. + * @param string $value the string to expand. + * @param assoc $relative_to options for how to resolve relative IRIs: + * base: true to resolve against the base IRI, false not to. + * vocab: true to concatenate after @vocab, false not to. + * @param stdClass $local_ctx the local context being processed (only given + * if called during document processing). + * @param defined a map for tracking cycles in context definitions (only given + * if called during document processing). + * + * @return mixed the expanded value. + */ + function _expandIri( + $active_ctx, $value, $relative_to = [], $local_ctx = null, $defined = null) + { + // already expanded + if ($value === null || self::_isKeyword($value)) { + return $value; + } + + // define term dependency if not defined + if ($local_ctx !== null && property_exists($local_ctx, $value) && + !self::_hasKeyValue($defined, $value, true)) { + $this->_createTermDefinition($active_ctx, $local_ctx, $value, $defined); + } + + if (isset($relative_to['vocab']) && $relative_to['vocab']) { + if (property_exists($active_ctx->mappings, $value)) { + $mapping = $active_ctx->mappings->{$value}; + + // value is explicitly ignored with a null mapping + if ($mapping === null) { + return null; + } + + // value is a term + return $mapping->{'@id'}; + } + } + + // split value into prefix:suffix + $colon = strpos($value, ':'); + if ($colon !== false) { + $prefix = substr($value, 0, $colon); + $suffix = substr($value, $colon + 1); + + // do not expand blank nodes (prefix of '_') or already-absolute + // IRIs (suffix of '//') + if ($prefix === '_' || strpos($suffix, '//') === 0) { + return $value; + } + + // prefix dependency not defined, define it + if ($local_ctx !== null && property_exists($local_ctx, $prefix)) { + $this->_createTermDefinition( + $active_ctx, $local_ctx, $prefix, $defined); + } + + // use mapping if prefix is defined + if (property_exists($active_ctx->mappings, $prefix)) { + $mapping = $active_ctx->mappings->{$prefix}; + if ($mapping) { + return $mapping->{'@id'} . $suffix; + } + } + + // already absolute IRI + return $value; + } + + // prepend vocab + if (isset($relative_to['vocab']) && $relative_to['vocab'] && + property_exists($active_ctx, '@vocab')) { + return $active_ctx->{'@vocab'} . $value; + } + + // prepend base + $rval = $value; + if (isset($relative_to['base']) && $relative_to['base']) { + $rval = jsonld_prepend_base($active_ctx->{'@base'}, $rval); + } + + return $rval; + } + + /** + * Finds all @context URLs in the given JSON-LD input. + * + * @param mixed $input the JSON-LD input. + * @param stdClass $urls a map of URLs (url => false/@contexts). + * @param bool $replace true to replace the URLs in the given input with + * the @contexts from the urls map, false not to. + * @param string $base the base URL to resolve relative URLs with. + */ + protected function _findContextUrls($input, $urls, $replace, $base) + { + if (is_array($input)) { + foreach ($input as $e) { + $this->_findContextUrls($e, $urls, $replace, $base); + } + } else if (is_object($input)) { + foreach ($input as $k => &$v) { + if ($k !== '@context') { + $this->_findContextUrls($v, $urls, $replace, $base); + continue; + } + + // array @context + if (is_array($v)) { + $length = count($v); + for ($i = 0; $i < $length; ++$i) { + if (is_string($v[$i])) { + $url = jsonld_prepend_base($base, $v[$i]); + // replace w/@context if requested + if ($replace) { + $ctx = $urls->{$url}; + if (is_array($ctx)) { + // add flattened context + array_splice($v, $i, 1, $ctx); + $i += count($ctx) - 1; + $length = count($v); + } else { + $v[$i] = $ctx; + } + } else if (!property_exists($urls, $url)) { + // @context URL found + $urls->{$url} = false; + } + } + } + } else if (is_string($v)) { + // string @context + $v = jsonld_prepend_base($base, $v); + // replace w/@context if requested + if ($replace) { + $input->{$k} = $urls->{$v}; + } else if (!property_exists($urls, $v)) { + // @context URL found + $urls->{$v} = false; + } + } + } + } + } + + /** + * Retrieves external @context URLs using the given document loader. Each + * instance of @context in the input that refers to a URL will be replaced + * with the JSON @context found at that URL. + * + * @param mixed $input the JSON-LD input with possible contexts. + * @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. + * + * @return mixed the result. + */ + protected function _retrieveContextUrls( + &$input, $cycles, $load_document, $base = '') + { + if (count(get_object_vars($cycles)) > self::MAX_CONTEXT_URLS) { + throw new JsonLdException( + 'Maximum number of @context URLs exceeded.', 'jsonld.ContextUrlError', 'loading remote context failed', ['max' => self::MAX_CONTEXT_URLS]); + } + + // for tracking the URLs to retrieve + $urls = new stdClass(); + + // regex for validating URLs + $regex = '/(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/'; + + // find all URLs in the given input + $this->_findContextUrls($input, $urls, false, $base); + + // queue all unretrieved URLs + $queue = []; + foreach ($urls as $url => $ctx) { + if ($ctx === false) { + // validate URL + if (!preg_match($regex, $url)) { + throw new JsonLdException( + 'Malformed or unsupported URL.', 'jsonld.InvalidUrl', 'loading remote context failed', ['url' => $url]); + } + $queue[] = $url; + } + } + + // retrieve URLs in queue + foreach ($queue as $url) { + // check for context URL cycle + if (property_exists($cycles, $url)) { + throw new JsonLdException( + 'Cyclical @context URLs detected.', 'jsonld.ContextUrlError', 'recursive context inclusion', ['url' => $url]); + } + $_cycles = self::copy($cycles); + $_cycles->{$url} = true; + + // retrieve URL + $remote_doc = call_user_func($load_document, $url); + $ctx = $remote_doc->document; + + // parse string context as JSON + if (is_string($ctx)) { + try { + $ctx = self::_parse_json($ctx); + } catch (Exception $e) { + 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( + 'Derefencing a URL did not result in a valid JSON-LD object.', 'jsonld.InvalidUrl', 'invalid remote context', ['url' => $url]); + } + + // use empty context if no @context key is present + if (!property_exists($ctx, '@context')) { + $ctx = (object) ['@context' => new stdClass()]; + } else { + $ctx = (object) ['@context' => $ctx->{'@context'}]; + } + + // append context URL to context if given + if ($remote_doc->contextUrl !== null) { + $ctx->{'@context'} = self::arrayify($ctx->{'@context'}); + $ctx->{'@context'}[] = $remote_doc->contextUrl; + } + + // recurse + $this->_retrieveContextUrls($ctx, $_cycles, $load_document, $url); + $urls->{$url} = $ctx->{'@context'}; + } + + // replace all URLS in the input + $this->_findContextUrls($input, $urls, true, $base); + } + + /** + * Gets the initial context. + * + * @param assoc $options the options to use. + * base the document base IRI. + * + * @return stdClass the initial context. + */ + protected function _getInitialContext($options) + { + return (object) [ + '@base' => jsonld_parse_url($options['base']), + 'mappings' => new stdClass(), + 'inverse' => null]; + } + + /** + * Generates an inverse context for use in the compaction algorithm, if + * not already generated for the given active context. + * + * @param stdClass $active_ctx the active context to use. + * + * @return stdClass the inverse context. + */ + protected function _getInverseContext($active_ctx) + { + // inverse context already generated + if ($active_ctx->inverse) { + return $active_ctx->inverse; + } + + $inverse = $active_ctx->inverse = new stdClass(); + + // variables for building fast CURIE map + $fast_curie_map = $active_ctx->fast_curie_map = new ArrayObject(); + $iris_to_terms = []; + + // handle default language + $default_language = '@none'; + if (property_exists($active_ctx, '@language')) { + $default_language = $active_ctx->{'@language'}; + } + + // create term selections for each mapping in the context, ordered by + // shortest and then lexicographically least + $mappings = $active_ctx->mappings; + $terms = array_keys((array) $mappings); + usort($terms, [$this, '_compareShortestLeast']); + foreach ($terms as $term) { + $mapping = $mappings->{$term}; + if ($mapping === null) { + continue; + } + + // add term selection where it applies + if (property_exists($mapping, '@container')) { + $container = $mapping->{'@container'}; + } else { + $container = '@none'; + } + + // iterate over every IRI in the mapping + $iris = $mapping->{'@id'}; + $iris = self::arrayify($iris); + foreach ($iris as $iri) { + $is_keyword = self::_isKeyword($iri); + + // initialize container map + if (!property_exists($inverse, $iri)) { + $inverse->{$iri} = new stdClass(); + if (!$is_keyword && !$mapping->_term_has_colon) { + // init IRI to term map and fast CURIE map + $iris_to_terms[$iri] = new ArrayObject(); + $iris_to_terms[$iri][] = $term; + $fast_curie_entry = (object) [ + 'iri' => $iri, 'terms' => $iris_to_terms[$iri]]; + if (!array_key_exists($iri[0], (array) $fast_curie_map)) { + $fast_curie_map[$iri[0]] = new ArrayObject(); + } + $fast_curie_map[$iri[0]][] = $fast_curie_entry; + } + } else if (!$is_keyword && !$mapping->_term_has_colon) { + // add IRI to term match + $iris_to_terms[$iri][] = $term; + } + $container_map = $inverse->{$iri}; + + // add new entry + if (!property_exists($container_map, $container)) { + $container_map->{$container} = (object) [ + '@language' => new stdClass(), + '@type' => new stdClass()]; + } + $entry = $container_map->{$container}; + + if ($mapping->reverse) { + // term is preferred for values using @reverse + $this->_addPreferredTerm( + $mapping, $term, $entry->{'@type'}, '@reverse'); + } else if (property_exists($mapping, '@type')) { + // term is preferred for values using specific type + $this->_addPreferredTerm( + $mapping, $term, $entry->{'@type'}, $mapping->{'@type'}); + } else if (property_exists($mapping, '@language')) { + // term is preferred for values using specific language + $language = $mapping->{'@language'}; + if ($language === null) { + $language = '@null'; + } + $this->_addPreferredTerm( + $mapping, $term, $entry->{'@language'}, $language); + } else { + // term is preferred for values w/default language or no type and + // no language + // add an entry for the default language + $this->_addPreferredTerm( + $mapping, $term, $entry->{'@language'}, $default_language); + + // add entries for no type and no language + $this->_addPreferredTerm( + $mapping, $term, $entry->{'@type'}, '@none'); + $this->_addPreferredTerm( + $mapping, $term, $entry->{'@language'}, '@none'); + } + } + } + + // build fast CURIE map + foreach ($fast_curie_map as $key => $value) { + $this->_buildIriMap($fast_curie_map, $key, 1); + } + + return $inverse; + } + + /** + * Runs a recursive algorithm to build a lookup map for quickly finding + * potential CURIEs. + * + * @param ArrayObject $iri_map the map to build. + * @param string $key the current key in the map to work on. + * @param int $idx the index into the IRI to compare. + */ + function _buildIriMap($iri_map, $key, $idx) + { + $entries = $iri_map[$key]; + $next = $iri_map[$key] = new ArrayObject(); + + foreach ($entries as $entry) { + $iri = $entry->iri; + if ($idx >= strlen($iri)) { + $letter = ''; + } else { + $letter = $iri[$idx]; + } + if (!isset($next[$letter])) { + $next[$letter] = new ArrayObject(); + } + $next[$letter][] = $entry; + } + + foreach ($next as $key => $value) { + if ($key === '') { + continue; + } + $this->_buildIriMap($next, $key, $idx + 1); + } + } + + /** + * Adds the term for the given entry if not already added. + * + * @param stdClass $mapping the term mapping. + * @param string $term the term to add. + * @param stdClass $entry the inverse context type_or_language entry to + * add to. + * @param string $type_or_language_value the key in the entry to add to. + */ + function _addPreferredTerm($mapping, $term, $entry, $type_or_language_value) + { + if (!property_exists($entry, $type_or_language_value)) { + $entry->{$type_or_language_value} = $term; + } + } + + /** + * Clones an active context, creating a child active context. + * + * @return stdClass a clone (child) of the active context. + */ + protected function _cloneActiveContext($active_ctx) + { + $child = new stdClass(); + $child->{'@base'} = $active_ctx->{'@base'}; + $child->mappings = self::copy($active_ctx->mappings); + $child->inverse = null; + if (property_exists($active_ctx, '@language')) { + $child->{'@language'} = $active_ctx->{'@language'}; + } + if (property_exists($active_ctx, '@vocab')) { + $child->{'@vocab'} = $active_ctx->{'@vocab'}; + } + return $child; + } + + /** + * Returns whether or not the given value is a keyword. + * + * @param string $v the value to check. + * + * @return bool true if the value is a keyword, false if not. + */ + protected static function _isKeyword($v) + { + if (!is_string($v)) { + return false; + } + switch ($v) { + case '@base': + case '@context': + case '@container': + case '@default': + case '@embed': + case '@explicit': + case '@graph': + case '@id': + case '@index': + case '@language': + case '@list': + case '@omitDefault': + case '@preserve': + case '@requireAll': + case '@reverse': + case '@set': + case '@type': + case '@value': + case '@vocab': + return true; + } + return false; + } + + /** + * Returns true if the given value is an empty Object. + * + * @param mixed $v the value to check. + * + * @return bool true if the value is an empty Object, false if not. + */ + protected static function _isEmptyObject($v) + { + return is_object($v) && count(get_object_vars($v)) === 0; + } + + /** + * Throws an exception if the given value is not a valid @type value. + * + * @param mixed $v the value to check. + */ + protected static function _validateTypeValue($v) + { + // must be a string or empty object + if (is_string($v) || self::_isEmptyObject($v)) { + return; + } + + // must be an array + $is_valid = false; + if (is_array($v)) { + // must contain only strings + $is_valid = true; + foreach ($v as $e) { + if (!(is_string($e))) { + $is_valid = false; + break; + } + } + } + + if (!$is_valid) { + throw new JsonLdException( + 'Invalid JSON-LD syntax; "@type" value must a string, an array ' . + 'of strings, or an empty object.', 'jsonld.SyntaxError', 'invalid type value', ['value' => $v]); + } + } + + /** + * Returns true if the given value is a subject with properties. + * + * @param mixed $v the value to check. + * + * @return bool true if the value is a subject with properties, false if not. + */ + protected static function _isSubject($v) + { + // Note: A value is a subject if all of these hold true: + // 1. It is an Object. + // 2. It is not a @value, @set, or @list. + // 3. It has more than 1 key OR any existing key is not @id. + $rval = false; + if (is_object($v) && + !property_exists($v, '@value') && + !property_exists($v, '@set') && + !property_exists($v, '@list')) { + $count = count(get_object_vars($v)); + $rval = ($count > 1 || !property_exists($v, '@id')); + } + return $rval; + } + + /** + * Returns true if the given value is a subject reference. + * + * @param mixed $v the value to check. + * + * @return bool true if the value is a subject reference, false if not. + */ + protected static function _isSubjectReference($v) + { + // Note: A value is a subject reference if all of these hold true: + // 1. It is an Object. + // 2. It has a single key: @id. + return (is_object($v) && count(get_object_vars($v)) === 1 && + property_exists($v, '@id')); + } + + /** + * Returns true if the given value is a @value. + * + * @param mixed $v the value to check. + * + * @return bool true if the value is a @value, false if not. + */ + protected static function _isValue($v) + { + // Note: A value is a @value if all of these hold true: + // 1. It is an Object. + // 2. It has the @value property. + return is_object($v) && property_exists($v, '@value'); + } + + /** + * Returns true if the given value is a @list. + * + * @param mixed $v the value to check. + * + * @return bool true if the value is a @list, false if not. + */ + protected static function _isList($v) + { + // Note: A value is a @list if all of these hold true: + // 1. It is an Object. + // 2. It has the @list property. + return is_object($v) && property_exists($v, '@list'); + } + + /** + * Returns true if the given value is a blank node. + * + * @param mixed $v the value to check. + * + * @return bool true if the value is a blank node, false if not. + */ + protected static function _isBlankNode($v) + { + // Note: A value is a blank node if all of these hold true: + // 1. It is an Object. + // 2. If it has an @id key its value begins with '_:'. + // 3. It has no keys OR is not a @value, @set, or @list. + $rval = false; + if (is_object($v)) { + if (property_exists($v, '@id')) { + $rval = (strpos($v->{'@id'}, '_:') === 0); + } else { + $rval = (count(get_object_vars($v)) === 0 || + !(property_exists($v, '@value') || + property_exists($v, '@set') || + property_exists($v, '@list'))); + } + } + return $rval; + } + + /** + * Returns true if the given value is an absolute IRI, false if not. + * + * @param string $v the value to check. + * + * @return bool true if the value is an absolute IRI, false if not. + */ + protected static function _isAbsoluteIri($v) + { + return strpos($v, ':') !== false; + } + + /** + * Returns true if the given target has the given key and its + * value equals is the given value. + * + * @param stdClass $target the target object. + * @param string key the key to check. + * @param mixed $value the value to check. + * + * @return bool true if the target has the given key and its value matches. + */ + protected static function _hasKeyValue($target, $key, $value) + { + return (property_exists($target, $key) && $target->{$key} === $value); + } + + /** + * Returns true if both of the given objects have the same value for the + * given key or if neither of the objects contain the given key. + * + * @param stdClass $o1 the first object. + * @param stdClass $o2 the second object. + * @param string key the key to check. + * + * @return bool true if both objects have the same value for the key or + * neither has the key. + */ + protected static function _compareKeyValues($o1, $o2, $key) + { + if (property_exists($o1, $key)) { + return property_exists($o2, $key) && $o1->{$key} === $o2->{$key}; + } + return !property_exists($o2, $key); + } + + /** + * Parses JSON and sets an appropriate exception message on error. + * + * @param string $json the JSON to parse. + * + * @return mixed the parsed JSON object or array. + */ + protected static function _parse_json($json) + { + $rval = json_decode($json); + $error = json_last_error(); + if ($error === JSON_ERROR_NONE && $rval === null) { + $error = JSON_ERROR_SYNTAX; + } + switch ($error) { + case JSON_ERROR_NONE: + break; + case JSON_ERROR_DEPTH: + throw new JsonLdException( + 'Could not parse JSON; the maximum stack depth has been exceeded.', 'jsonld.ParseError'); + case JSON_ERROR_STATE_MISMATCH: + throw new JsonLdException( + 'Could not parse JSON; invalid or malformed JSON.', 'jsonld.ParseError'); + case JSON_ERROR_CTRL_CHAR: + case JSON_ERROR_SYNTAX: + throw new JsonLdException( + 'Could not parse JSON; syntax error, malformed JSON.', 'jsonld.ParseError'); + case JSON_ERROR_UTF8: + throw new JsonLdException( + 'Could not parse JSON from URL; malformed UTF-8 characters.', 'jsonld.ParseError'); + default: + throw new JsonLdException( + 'Could not parse JSON from URL; unknown error.', 'jsonld.ParseError'); + } + return $rval; + } + } // register the N-Quads RDF parser jsonld_register_rdf_parser( - 'application/nquads', array('JsonLdProcessor', 'parseNQuads')); +'application/nquads', ['JsonLdProcessor', 'parseNQuads']); /** * A JSON-LD Exception. */ -class JsonLdException extends Exception { - public function __construct( - $msg, $type, $code='error', $details=null, $previous=null) { - $this->type = $type; - $this->code = $code; - $this->details = $details; - $this->cause = $previous; - parent::__construct($msg, 0, $previous); - } - public function __toString() { - $rval = __CLASS__ . ": [{$this->type}]: {$this->message}\n"; - if($this->code) { - $rval .= 'Code: ' . $this->code . "\n"; - } - if($this->details) { - $rval .= 'Details: ' . print_r($this->details, true) . "\n"; - } - if($this->cause) { - $rval .= 'Cause: ' . $this->cause; - } - $rval .= $this->getTraceAsString() . "\n"; - return $rval; - } -}; +class JsonLdException extends Exception +{ + + public function __construct( + $msg, $type, $code = 'error', $details = null, $previous = null) + { + $this->type = $type; + $this->code = $code; + $this->details = $details; + $this->cause = $previous; + parent::__construct($msg, 0, $previous); + } + + public function __toString() + { + $rval = __CLASS__ . ": [{$this->type}]: {$this->message}\n"; + if ($this->code) { + $rval .= 'Code: ' . $this->code . "\n"; + } + if ($this->details) { + $rval .= 'Details: ' . print_r($this->details, true) . "\n"; + } + if ($this->cause) { + $rval .= 'Cause: ' . $this->cause; + } + $rval .= $this->getTraceAsString() . "\n"; + return $rval; + } + +} + +; /** * A UniqueNamer issues unique names, keeping track of any previously issued * names. */ -class UniqueNamer { - /** - * Constructs a new UniqueNamer. - * - * @param prefix the prefix to use (''). - */ - public function __construct($prefix) { - $this->prefix = $prefix; - $this->counter = 0; - $this->existing = new stdClass(); - $this->order = array(); - } +class UniqueNamer +{ - /** - * Clones this UniqueNamer. - */ - public function __clone() { - $this->existing = clone $this->existing; - } + /** + * Constructs a new UniqueNamer. + * + * @param prefix the prefix to use (''). + */ + 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 - * 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}; - } + /** + * Clones this UniqueNamer. + */ + public function __clone() + { + $this->existing = clone $this->existing; + } - // get next name - $name = $this->prefix . $this->counter; - $this->counter += 1; + /** + * Gets the new name for the given old name, where if no old name is given + * 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 - if($old_name !== null) { - $this->existing->{$old_name} = $name; - $this->order[] = $old_name; - } + // get next name + $name = $this->prefix . $this->counter; + $this->counter += 1; - 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 * of elements. */ -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; - } - } +class Permutator +{ - /** - * Returns true if there is another permutation. - * - * @return bool true if there is another permutation, false if not. - */ - public function hasNext() { - return !$this->done; - } + /** + * 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; + } + } - /** - * 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; + /** + * Returns true if there is another permutation. + * + * @return bool true if there is another permutation, false if not. + */ + public function hasNext() + { + return !$this->done; + } - /* 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 - // (mobile: element is greater than the one it is looking at) - $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; - } - } + /* Calculate the next permutation using the Steinhaus-Johnson-Trotter + permutation algorithm. */ - // no more permutations - if($k === null) { - $this->done = true; - } 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; + // get largest mobile element k + // (mobile: element is greater than the one it is looking at) + $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; + } + } - // 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]}; - } - } - } + // no more permutations + if ($k === null) { + $this->done = true; + } 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 * the overhead of recomputing them. */ -class ActiveContextCache { - /** - * Constructs a new ActiveContextCache. - * - * @param int size the maximum size of the cache, defaults to 100. - */ - public function __construct($size=100) { - $this->order = array(); - $this->cache = new stdClass(); - $this->size = $size; - } +class ActiveContextCache +{ + /** + * Constructs a new ActiveContextCache. + * + * @param int size the maximum size of the cache, defaults to 100. + */ + public function __construct($size = 100) + { + $this->order = []; + $this->cache = new stdClass(); + $this->size = $size; + } - /** - * Gets an active context from the cache based on the current active - * context and the new local context. - * - * @param stdClass $active_ctx the current active context. - * @param stdClass $local_ctx the new local context. - * - * @return mixed a shared copy of the cached active context or null. - */ - public function get($active_ctx, $local_ctx) { - $key1 = serialize($active_ctx); - $key2 = serialize($local_ctx); - if(property_exists($this->cache, $key1)) { - $level1 = $this->cache->{$key1}; - if(property_exists($level1, $key2)) { - return $level1->{$key2}; - } - } - return null; - } + /** + * Gets an active context from the cache based on the current active + * context and the new local context. + * + * @param stdClass $active_ctx the current active context. + * @param stdClass $local_ctx the new local context. + * + * @return mixed a shared copy of the cached active context or null. + */ + public function get($active_ctx, $local_ctx) + { + $key1 = serialize($active_ctx); + $key2 = serialize($local_ctx); + if (property_exists($this->cache, $key1)) { + $level1 = $this->cache->{$key1}; + if (property_exists($level1, $key2)) { + return $level1->{$key2}; + } + } - /** - * 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. - * @param stdClass $local_ctx the just-processed local context. - * @param stdClass $result the resulting active context. - */ - public function set($active_ctx, $local_ctx, $result) { - if(count($this->order) === $this->size) { - $entry = array_shift($this->order); - unset($this->cache->{$entry->activeCtx}->{$entry->localCtx}); - } - $key1 = serialize($active_ctx); - $key2 = serialize($local_ctx); - $this->order[] = (object)array( - 'activeCtx' => $key1, 'localCtx' => $key2); - if(!property_exists($this->cache, $key1)) { - $this->cache->{$key1} = new stdClass(); - } - $this->cache->{$key1}->{$key2} = JsonLdProcessor::copy($result); - } + 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. + * @param stdClass $local_ctx the just-processed local context. + * @param stdClass $result the resulting active context. + */ + public function set($active_ctx, $local_ctx, $result) + { + if (count($this->order) === $this->size) { + $entry = array_shift($this->order); + unset($this->cache->{$entry->activeCtx}->{$entry->localCtx}); + } + + $key1 = serialize($active_ctx); + $key2 = serialize($local_ctx); + $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 ?> */ diff --git a/test.php b/test.php index 11b72ac..93cb015 100644 --- a/test.php +++ b/test.php @@ -1,4 +1,5 @@ attach($result); - $this->result = $result; - parent::run($result); - } +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. + * + * @param PHPUnit_Framework_TestResult $result the test result. + */ + public function run(PHPUnit_Framework_TestResult $result = NULL) + { + global $EARL; + $EARL->attach($result); + $this->result = $result; + parent::run($result); + } - /** - * Tests expansion. - * - * @param JsonLdTest $test the test to run. - * - * @group expand - * @group json-ld.org - * @dataProvider expandProvider - */ - public function testExpand($test) { - $this->test = $test; - $input = $test->readUrl('input'); - $options = $test->createOptions(); - $test->run('jsonld_expand', array($input, $options)); - } + /** + * Tests expansion. + * + * @param JsonLdTest $test the test to run. + * + * @group expand + * @group json-ld.org + * @dataProvider expandProvider + */ + public function testExpand($test) + { + $this->test = $test; + $input = $test->readUrl('input'); + $options = $test->createOptions(); + $test->run('jsonld_expand', [$input, $options]); + } - /** - * Tests compaction. - * - * @param JsonLdTest $test the test to run. - * - * @group compact - * @group json-ld.org - * @dataProvider compactProvider - */ - public function testCompact($test) { - $this->test = $test; - $input = $test->readUrl('input'); - $context = $test->readProperty('context'); - $options = $test->createOptions(); - $test->run('jsonld_compact', array($input, $context, $options)); - } + /** + * Tests compaction. + * + * @param JsonLdTest $test the test to run. + * + * @group compact + * @group json-ld.org + * @dataProvider compactProvider + */ + public function testCompact($test) + { + $this->test = $test; + $input = $test->readUrl('input'); + $context = $test->readProperty('context'); + $options = $test->createOptions(); + $test->run('jsonld_compact', [$input, $context, $options]); + } - /** - * Tests flatten. - * - * @param JsonLdTest $test the test to run. - * - * @group flatten - * @group json-ld.org - * @dataProvider flattenProvider - */ - public function testFlatten($test) { - $this->test = $test; - $input = $test->readUrl('input'); - $context = $test->readProperty('context'); - $options = $test->createOptions(); - $test->run('jsonld_flatten', array($input, $context, $options)); - } + /** + * Tests flatten. + * + * @param JsonLdTest $test the test to run. + * + * @group flatten + * @group json-ld.org + * @dataProvider flattenProvider + */ + public function testFlatten($test) + { + $this->test = $test; + $input = $test->readUrl('input'); + $context = $test->readProperty('context'); + $options = $test->createOptions(); + $test->run('jsonld_flatten', [$input, $context, $options]); + } - /** - * Tests serialization to RDF. - * - * @param JsonLdTest $test the test to run. - * - * @group toRdf - * @group json-ld.org - * @dataProvider toRdfProvider - */ - public function testToRdf($test) { - $this->test = $test; - $input = $test->readUrl('input'); - $options = $test->createOptions(array('format' => 'application/nquads')); - $test->run('jsonld_to_rdf', array($input, $options)); - } + /** + * Tests serialization to RDF. + * + * @param JsonLdTest $test the test to run. + * + * @group toRdf + * @group json-ld.org + * @dataProvider toRdfProvider + */ + public function testToRdf($test) + { + $this->test = $test; + $input = $test->readUrl('input'); + $options = $test->createOptions(['format' => 'application/nquads']); + $test->run('jsonld_to_rdf', [$input, $options]); + } - /** - * Tests deserialization from RDF. - * - * @param JsonLdTest $test the test to run. - * - * @group fromRdf - * @group json-ld.org - * @dataProvider fromRdfProvider - */ - public function testFromRdf($test) { - $this->test = $test; - $input = $test->readProperty('input'); - $options = $test->createOptions(array('format' => 'application/nquads')); - $test->run('jsonld_from_rdf', array($input, $options)); - } + /** + * Tests deserialization from RDF. + * + * @param JsonLdTest $test the test to run. + * + * @group fromRdf + * @group json-ld.org + * @dataProvider fromRdfProvider + */ + public function testFromRdf($test) + { + $this->test = $test; + $input = $test->readProperty('input'); + $options = $test->createOptions(['format' => 'application/nquads']); + $test->run('jsonld_from_rdf', [$input, $options]); + } - /** - * Tests framing. - * - * @param JsonLdTest $test the test to run. - * - * @group frame - * @group json-ld.org - * @dataProvider frameProvider - */ - public function testFrame($test) { - $this->test = $test; - $input = $test->readUrl('input'); - $frame = $test->readProperty('frame'); - $options = $test->createOptions(); - $test->run('jsonld_frame', array($input, $frame, $options)); - } + /** + * Tests framing. + * + * @param JsonLdTest $test the test to run. + * + * @group frame + * @group json-ld.org + * @dataProvider frameProvider + */ + public function testFrame($test) + { + $this->test = $test; + $input = $test->readUrl('input'); + $frame = $test->readProperty('frame'); + $options = $test->createOptions(); + $test->run('jsonld_frame', [$input, $frame, $options]); + } - /** - * Tests normalization. - * - * @param JsonLdTest $test the test to run. - * - * @group normalize - * @group json-ld.org - * @dataProvider normalizeProvider - */ - public function testNormalize($test) { - $this->test = $test; - $input = $test->readUrl('input'); - $options = $test->createOptions(array('format' => 'application/nquads')); - $test->run('jsonld_normalize', array($input, $options)); - } + /** + * Tests normalization. + * + * @param JsonLdTest $test the test to run. + * + * @group normalize + * @group json-ld.org + * @dataProvider normalizeProvider + */ + public function testNormalize($test) + { + $this->test = $test; + $input = $test->readUrl('input'); + $options = $test->createOptions(['format' => 'application/nquads']); + $test->run('jsonld_normalize', [$input, $options]); + } - /** - * Tests URGNA2012 normalization. - * - * @param JsonLdTest $test the test to run. - * - * @group normalize - * @group normalization - * @dataProvider urgna2012Provider - */ - public function testUrgna2012($test) { - $this->test = $test; - $input = $test->readProperty('action'); - $options = $test->createOptions(array( - 'algorithm' => 'URGNA2012', - 'inputFormat' => 'application/nquads', - 'format' => 'application/nquads')); - $test->run('jsonld_normalize', array($input, $options)); - } + /** + * Tests URGNA2012 normalization. + * + * @param JsonLdTest $test the test to run. + * + * @group normalize + * @group normalization + * @dataProvider urgna2012Provider + */ + public function testUrgna2012($test) + { + $this->test = $test; + $input = $test->readProperty('action'); + $options = $test->createOptions([ + 'algorithm' => 'URGNA2012', + 'inputFormat' => 'application/nquads', + 'format' => 'application/nquads']); + $test->run('jsonld_normalize', [$input, $options]); + } - /** - * Tests URDNA2015 normalization. - * - * @param JsonLdTest $test the test to run. - * - * @group normalize - * @group normalization - * @dataProvider urdna2015Provider - */ - public function testUrdna2015($test) { - $this->test = $test; - $input = $test->readProperty('action'); - $options = $test->createOptions(array( - 'algorithm' => 'URDNA2015', - 'inputFormat' => 'application/nquads', - 'format' => 'application/nquads')); - $test->run('jsonld_normalize', array($input, $options)); - } + /** + * Tests URDNA2015 normalization. + * + * @param JsonLdTest $test the test to run. + * + * @group normalize + * @group normalization + * @dataProvider urdna2015Provider + */ + public function testUrdna2015($test) + { + $this->test = $test; + $input = $test->readProperty('action'); + $options = $test->createOptions([ + 'algorithm' => 'URDNA2015', + 'inputFormat' => 'application/nquads', + 'format' => 'application/nquads']); + $test->run('jsonld_normalize', [$input, $options]); + } - public function expandProvider() { - return new JsonLdTestIterator('jld:ExpandTest'); - } + public function expandProvider() + { + return new JsonLdTestIterator('jld:ExpandTest'); + } - public function compactProvider() { - return new JsonLdTestIterator('jld:CompactTest'); - } + public function compactProvider() + { + return new JsonLdTestIterator('jld:CompactTest'); + } - public function flattenProvider() { - return new JsonLdTestIterator('jld:FlattenTest'); - } + public function flattenProvider() + { + return new JsonLdTestIterator('jld:FlattenTest'); + } - public function toRdfProvider() { - return new JsonLdTestIterator('jld:ToRDFTest'); - } + public function toRdfProvider() + { + return new JsonLdTestIterator('jld:ToRDFTest'); + } - public function fromRdfProvider() { - return new JsonLdTestIterator('jld:FromRDFTest'); - } + public function fromRdfProvider() + { + return new JsonLdTestIterator('jld:FromRDFTest'); + } - public function normalizeProvider() { - return new JsonLdTestIterator('jld:NormalizeTest'); - } + public function normalizeProvider() + { + return new JsonLdTestIterator('jld:NormalizeTest'); + } - public function frameProvider() { - return new JsonLdTestIterator('jld:FrameTest'); - } + public function frameProvider() + { + return new JsonLdTestIterator('jld:FrameTest'); + } - public function urgna2012Provider() { - return new JsonLdTestIterator('rdfn:Urgna2012EvalTest'); - } + public function urgna2012Provider() + { + return new JsonLdTestIterator('rdfn:Urgna2012EvalTest'); + } - public function urdna2015Provider() { - return new JsonLdTestIterator('rdfn:Urdna2015EvalTest'); - } + public function urdna2015Provider() + { + return new JsonLdTestIterator('rdfn:Urdna2015EvalTest'); + } } -class JsonLdManifest { - public function __construct($data, $filename) { - $this->data = $data; - $this->filename = $filename; - $this->dirname = dirname($filename); - } +class JsonLdManifest +{ + public function __construct($data, $filename) + { + $this->data = $data; + $this->filename = $filename; + $this->dirname = dirname($filename); + } - public function load(&$tests) { - $entries = array_merge( - JsonLdProcessor::getValues($this->data, 'sequence'), - JsonLdProcessor::getValues($this->data, 'entries')); - $includes = JsonLdProcessor::getValues($this->data, 'include'); - foreach($includes as $include) { - array_push($entries, $include . '.jsonld'); - } - foreach($entries as $entry) { - if(is_string($entry)) { - $filename = join( - DIRECTORY_SEPARATOR, array($this->dirname, $entry)); - $entry = Util::readJson($filename); - } else { - $filename = $this->filename; - } + public function load(&$tests) + { + $entries = array_merge( + JsonLdProcessor::getValues($this->data, 'sequence'), JsonLdProcessor::getValues($this->data, 'entries')); + $includes = JsonLdProcessor::getValues($this->data, 'include'); + foreach ($includes as $include) { + array_push($entries, $include . '.jsonld'); + } + foreach ($entries as $entry) { + if (is_string($entry)) { + $filename = join( + DIRECTORY_SEPARATOR, [$this->dirname, $entry]); + $entry = Util::readJson($filename); + } else { + $filename = $this->filename; + } - if(JsonLdProcessor::hasValue($entry, '@type', 'mf:Manifest') || - JsonLdProcessor::hasValue($entry, 'type', 'mf:Manifest')) { - // entry is another manifest - $manifest = new JsonLdManifest($entry, $filename); - $manifest->load($tests); - } else { - // assume entry is a test - $test = new JsonLdTest($this, $entry, $filename); - $types = array_merge( - JsonLdProcessor::getValues($test->data, '@type'), - JsonLdProcessor::getValues($test->data, 'type')); - foreach($types as $type) { - if(!isset($tests[$type])) { - $tests[$type] = array(); - } - $tests[$type][] = $test; - } - } - } - } + if (JsonLdProcessor::hasValue($entry, '@type', 'mf:Manifest') || + JsonLdProcessor::hasValue($entry, 'type', 'mf:Manifest')) { + // entry is another manifest + $manifest = new JsonLdManifest($entry, $filename); + $manifest->load($tests); + } else { + // assume entry is a test + $test = new JsonLdTest($this, $entry, $filename); + $types = array_merge( + JsonLdProcessor::getValues($test->data, '@type'), JsonLdProcessor::getValues($test->data, 'type')); + foreach ($types as $type) { + if (!isset($tests[$type])) { + $tests[$type] = []; + } + $tests[$type][] = $test; + } + } + } + } } -class JsonLdTest { - public function __construct($manifest, $data, $filename) { - $this->manifest = $manifest; - $this->data = $data; - $this->filename = $filename; - $this->dirname = dirname($filename); - $this->isPositive = - JsonLdProcessor::hasValue( - $data, '@type', 'jld:PositiveEvaluationTest') || - JsonLdProcessor::hasValue( - $data, 'type', 'jld:PositiveEvaluationTest'); - $this->isNegative = - JsonLdProcessor::hasValue( - $data, '@type', 'jld:NegativeEvaluationTest') || - JsonLdProcessor::hasValue( - $data, 'type', 'jld:NegativeEvaluationTest'); +class JsonLdTest +{ + public function __construct($manifest, $data, $filename) + { + $this->manifest = $manifest; + $this->data = $data; + $this->filename = $filename; + $this->dirname = dirname($filename); + $this->isPositive = JsonLdProcessor::hasValue( + $data, '@type', 'jld:PositiveEvaluationTest') || + JsonLdProcessor::hasValue( + $data, 'type', 'jld:PositiveEvaluationTest'); + $this->isNegative = JsonLdProcessor::hasValue( + $data, '@type', 'jld:NegativeEvaluationTest') || + JsonLdProcessor::hasValue( + $data, 'type', 'jld:NegativeEvaluationTest'); - // generate test name - if(isset($manifest->data->name)) { - $manifestLabel = $manifest->data->name; - } else if(isset($manifest->data->label)) { - $manifestLabel = $manifest->data->label; - } else { - $manifestLabel = 'UNNAMED'; - } - if(isset($this->data->id)) { - $testId = $this->data->id; - } else { - $testId = $this->data->{'@id'}; - } - if(isset($this->data->name)) { - $testLabel = $this->data->name; - } else if(isset($this->data->label)) { - $testLabel = $this->data->label; - } else { - $testLabel = 'UNNAMED'; - } - - $this->name = $manifestLabel . ' ' . $testId . ' - ' . $testLabel; + // generate test name + if (isset($manifest->data->name)) { + $manifestLabel = $manifest->data->name; + } else if (isset($manifest->data->label)) { + $manifestLabel = $manifest->data->label; + } else { + $manifestLabel = 'UNNAMED'; + } + if (isset($this->data->id)) { + $testId = $this->data->id; + } else { + $testId = $this->data->{'@id'}; + } + if (isset($this->data->name)) { + $testLabel = $this->data->name; + } else if (isset($this->data->label)) { + $testLabel = $this->data->label; + } else { + $testLabel = 'UNNAMED'; + } - // expand @id and input base - if(isset($manifest->data->baseIri)) { - $data->{'@id'} = ($manifest->data->baseIri . - basename($manifest->filename) . $data->{'@id'}); - $this->base = $manifest->data->baseIri . $data->input; - } - } + $this->name = $manifestLabel . ' ' . $testId . ' - ' . $testLabel; - private function _getResultProperty() { - if(isset($this->data->expect)) { - return 'expect'; - } else if(isset($this->data->result)) { - return 'result'; - } else { - throw new Exception('No test result property found.'); - } - } + // expand @id and input base + if (isset($manifest->data->baseIri)) { + $data->{'@id'} = ($manifest->data->baseIri . + basename($manifest->filename) . $data->{'@id'}); + $this->base = $manifest->data->baseIri . $data->input; + } + } - public function run($fn, $params) { - // read expected data - if($this->isNegative) { - $this->expected = $this->data->expect; - } else { - $this->expected = $this->readProperty($this->_getResultProperty()); - } + private function _getResultProperty() + { + if (isset($this->data->expect)) { + return 'expect'; + } else if (isset($this->data->result)) { + return 'result'; + } else { + throw new Exception('No test result property found.'); + } + } - try { - $this->actual = call_user_func_array($fn, $params); - if($this->isNegative) { - throw new Exception('Expected an error; one was not raised.'); - } - 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 run($fn, $params) + { + // read expected data + if ($this->isNegative) { + $this->expected = $this->data->expect; + } else { + $this->expected = $this->readProperty($this->_getResultProperty()); + } - public function readUrl($property) { - if(!property_exists($this->data, $property)) { - return null; - } - return $this->manifest->data->baseIri . $this->data->{$property}; - } + try { + $this->actual = call_user_func_array($fn, $params); + if ($this->isNegative) { + throw new Exception('Expected an error; one was not raised.'); + } + 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) { - $data = $this->data; - if(!property_exists($data, $property)) { - return null; - } - $filename = join( - 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 readUrl($property) + { + if (!property_exists($this->data, $property)) { + return null; + } + return $this->manifest->data->baseIri . $this->data->{$property}; + } - public function createOptions($opts=array()) { - $http_options = array( - 'contentType', 'httpLink', 'httpStatus', 'redirectTo'); - $test_options = (property_exists($this->data, 'option') ? - $this->data->option : array()); - $options = array(); - foreach($test_options as $k => $v) { - if(!in_array($k, $http_options)) { - $options[$k] = $v; - } - } - $options['documentLoader'] = $this->createDocumentLoader(); - $options = array_merge($options, $opts); - if(isset($options['expandContext'])) { - $filename = join( - DIRECTORY_SEPARATOR, array($this->dirname, $options['expandContext'])); - $options['expandContext'] = Util::readJson($filename); - } - return $options; - } + public function readProperty($property) + { + $data = $this->data; + if (!property_exists($data, $property)) { + return null; + } + $filename = join( + DIRECTORY_SEPARATOR, [$this->dirname, $data->{$property}]); + $extension = pathinfo($filename, PATHINFO_EXTENSION); + if ($extension === 'jsonld') { + return Util::readJson($filename); + } - public function createDocumentLoader() { - $base = 'http://json-ld.org/test-suite'; - $test = $this; + return Util::readFile($filename); + } - $load_locally = function($url) use ($test, $base) { - $doc = (object)array( - 'contextUrl' => null, 'documentUrl' => $url, 'document' => null); - $options = (property_exists($test->data, 'option') ? - $test->data->option : null); - if($options and $url === $test->base) { - if(property_exists($options, 'redirectTo') && - property_exists($options, 'httpStatus') && - $options->httpStatus >= '300') { - $doc->documentUrl = ($test->manifest->data->baseIri . - $options->redirectTo); - } else if(property_exists($options, 'httpLink')) { - $content_type = (property_exists($options, 'contentType') ? - $options->contentType : null); - $extension = pathinfo($url, PATHINFO_EXTENSION); - if(!$content_type && $extension === 'jsonld') { - $content_type = 'application/ld+json'; - } - $link_header = $options->httpLink; - if(is_array($link_header)) { - $link_header = join(',', $link_header); - } - $link_header = jsonld_parse_link_header($link_header); - if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) { - $link_header = $link_header['http://www.w3.org/ns/json-ld#context']; - } else { - $link_header = null; - } - if($link_header && $content_type !== 'application/ld+json') { - if(is_array($link_header)) { - throw new Exception('multiple context link headers'); - } - $doc->contextUrl = $link_header->target; - } - } - } - global $ROOT_MANIFEST_DIR; - 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; - }; + public function createOptions($opts = []) + { + $http_options = [ + 'contentType', 'httpLink', 'httpStatus', 'redirectTo']; + $test_options = (property_exists($this->data, 'option') ? + $this->data->option : []); + $options = []; + foreach ($test_options as $k => $v) { + if (!in_array($k, $http_options)) { + $options[$k] = $v; + } + } - $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); - } + $options['documentLoader'] = $this->createDocumentLoader(); + $options = array_merge($options, $opts); + if (isset($options['expandContext'])) { + $filename = join( + DIRECTORY_SEPARATOR, [$this->dirname, $options['expandContext']]); + $options['expandContext'] = Util::readJson($filename); + } - // attempt to load locally - return call_user_func($load_locally, $url); - }; + return $options; + } - 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 { - /** - * The current test index. - */ - protected $index = 0; +class JsonLdTestIterator implements Iterator +{ + /** + * The current test index. + */ + protected $index = 0; - /** - * The total number of tests. - */ - protected $count = 0; + /** + * The total number of tests. + */ + protected $count = 0; - /** - * Creates a TestIterator. - * - * @param string $type the type of tests to iterate over. - */ - public function __construct($type) { - global $TESTS; - if(isset($TESTS[$type])) { - $this->tests = $TESTS[$type]; - } else { - $this->tests = array(); - } - $this->count = count($this->tests); - } + /** + * Creates a TestIterator. + * + * @param string $type the type of tests to iterate over. + */ + public function __construct($type) + { + global $TESTS; + if (isset($TESTS[$type])) { + $this->tests = $TESTS[$type]; + } else { + $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]); - } + $this->count = count($this->tests); + } - /** - * Gets the current test number. - * - * @return int the current test number. - */ - public function key() { - return $this->index; - } + /** + * Gets the parameters for the next test. + * + * @return assoc the parameters for the next test. + */ + public function current() + { + return ['test' => $this->tests[$this->index]]; + } - /** - * Proceeds to the next test. - */ - public function next() { - $this->index += 1; - } + /** + * Gets the current test number. + * + * @return int the current test number. + */ + public function key() + { + return $this->index; + } - /** - * Rewinds to the first test. - */ - public function rewind() { - $this->index = 0; - } + /** + * Proceeds to the next test. + */ + public function next() + { + $this->index += 1; + } - /** - * 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; - } + /** + * Rewinds to the first test. + */ + public function rewind() + { + $this->index = 0; + } + + /** + * 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 - implements PHPUnit_Framework_TestListener { - public function __construct() { - $this->filename = null; - $this->attached = false; - $this->report = (object)array( - '@context' => (object)array( - 'doap' => 'http://usefulinc.com/ns/doap#', - 'foaf' => 'http://xmlns.com/foaf/0.1/', - 'dc' => 'http://purl.org/dc/terms/', - 'earl' => 'http://www.w3.org/ns/earl#', - 'xsd' => 'http://www.w3.org/2001/XMLSchema#', - 'doap:homepage' => (object)array('@type' => '@id'), - 'doap:license' => (object)array('@type' => '@id'), - 'dc:creator' => (object)array('@type' => '@id'), - 'foaf:homepage' => (object)array('@type' => '@id'), - 'subjectOf' => (object)array('@reverse' => 'earl:subject'), - 'earl:assertedBy' => (object)array('@type' => '@id'), - 'earl:mode' => (object)array('@type' => '@id'), - 'earl:test' => (object)array('@type' => '@id'), - 'earl:outcome' => (object)array('@type' => '@id'), - 'dc:date' => (object)array('@type' => 'xsd:date') - ), - '@id' => 'https://github.com/digitalbazaar/php-json-ld', - '@type' => array('doap:Project', 'earl:TestSubject', 'earl:Software'), - 'doap:name' => 'php-json-ld', - 'dc:title' => 'php-json-ld', - 'doap:homepage' => 'https://github.com/digitalbazaar/php-json-ld', - 'doap:license' => 'https://github.com/digitalbazaar/php-json-ld/blob/master/LICENSE', - 'doap:description' => 'A JSON-LD processor for PHP', - 'doap:programming-language' => 'PHP', - 'dc:creator' => 'https://github.com/dlongley', - 'doap:developer' => (object)array( - '@id' => 'https://github.com/dlongley', - '@type' => array('foaf:Person', 'earl:Assertor'), - 'foaf:name' => 'Dave Longley', - 'foaf:homepage' => 'https://github.com/dlongley' - ), - 'dc:date' => array( - '@value' => gmdate('Y-m-d'), - '@type' => 'xsd:date' - ), - 'subjectOf' => array() - ); - } +class EarlReport extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener +{ + public function __construct() + { + $this->filename = null; + $this->attached = false; + $this->report = (object) [ + '@context' => (object) [ + 'doap' => 'http://usefulinc.com/ns/doap#', + 'foaf' => 'http://xmlns.com/foaf/0.1/', + 'dc' => 'http://purl.org/dc/terms/', + 'earl' => 'http://www.w3.org/ns/earl#', + 'xsd' => 'http://www.w3.org/2001/XMLSchema#', + 'doap:homepage' => (object) ['@type' => '@id'], + 'doap:license' => (object) ['@type' => '@id'], + 'dc:creator' => (object) ['@type' => '@id'], + 'foaf:homepage' => (object) ['@type' => '@id'], + 'subjectOf' => (object) ['@reverse' => 'earl:subject'], + 'earl:assertedBy' => (object) ['@type' => '@id'], + 'earl:mode' => (object) ['@type' => '@id'], + 'earl:test' => (object) ['@type' => '@id'], + 'earl:outcome' => (object) ['@type' => '@id'], + 'dc:date' => (object) ['@type' => 'xsd:date'] + ], + '@id' => 'https://github.com/digitalbazaar/php-json-ld', + '@type' => ['doap:Project', 'earl:TestSubject', 'earl:Software'], + 'doap:name' => 'php-json-ld', + 'dc:title' => 'php-json-ld', + 'doap:homepage' => 'https://github.com/digitalbazaar/php-json-ld', + 'doap:license' => 'https://github.com/digitalbazaar/php-json-ld/blob/master/LICENSE', + 'doap:description' => 'A JSON-LD processor for PHP', + 'doap:programming-language' => 'PHP', + 'dc:creator' => 'https://github.com/dlongley', + 'doap:developer' => (object) [ + '@id' => 'https://github.com/dlongley', + '@type' => ['foaf:Person', 'earl:Assertor'], + 'foaf:name' => 'Dave Longley', + 'foaf:homepage' => 'https://github.com/dlongley' + ], + 'dc:date' => [ + '@value' => gmdate('Y-m-d'), + '@type' => 'xsd:date' + ], + 'subjectOf' => [] + ]; + } - /** - * Attaches to the given test result, if not yet attached. - * - * @param PHPUnit_Framework_Test $result the result to attach to. - */ - public function attach(PHPUnit_Framework_TestResult $result) { - if(!$this->attached && $this->filename) { - $this->attached = true; - $result->addListener($this); - } - } + /** + * Attaches to the given test result, if not yet attached. + * + * @param PHPUnit_Framework_Test $result the result to attach to. + */ + public function attach(PHPUnit_Framework_TestResult $result) + { + if (!$this->attached && $this->filename) { + $this->attached = true; + $result->addListener($this); + } + } - /** - * Adds an assertion to this EARL report. - * - * @param JsonLdTest $test the JsonLdTest for the assertion is for. - * @param bool $passed whether or not the test passed. - */ - public function addAssertion($test, $passed) { - $this->report->{'subjectOf'}[] = (object)array( - '@type' => 'earl:Assertion', - 'earl:assertedBy' => $this->report->{'doap:developer'}->{'@id'}, - 'earl:mode' => 'earl:automatic', - 'earl:test' => $test->data->{'@id'}, - 'earl:result' => (object)array( - '@type' => 'earl:TestResult', - 'dc:date' => gmdate(DateTime::ISO8601), - 'earl:outcome' => $passed ? 'earl:passed' : 'earl:failed' - ) - ); - return $this; - } + /** + * Adds an assertion to this EARL report. + * + * @param JsonLdTest $test the JsonLdTest for the assertion is for. + * @param bool $passed whether or not the test passed. + */ + public function addAssertion($test, $passed) + { + $this->report->{'subjectOf'}[] = (object) [ + '@type' => 'earl:Assertion', + 'earl:assertedBy' => $this->report->{'doap:developer'}->{'@id'}, + 'earl:mode' => 'earl:automatic', + 'earl:test' => $test->data->{'@id'}, + 'earl:result' => (object) [ + '@type' => 'earl:TestResult', + 'dc:date' => gmdate(DateTime::ISO8601), + 'earl:outcome' => $passed ? 'earl:passed' : 'earl:failed' + ] + ]; - /** - * 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); - } - } + return $this; + } - 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( - PHPUnit_Framework_Test $test, Exception $e, $time) { - $this->addAssertion($test->test, false); - } + public function endTest(PHPUnit_Framework_Test $test, $time) + { + $this->addAssertion($test->test, true); + } - public function addFailure( - PHPUnit_Framework_Test $test, - PHPUnit_Framework_AssertionFailedError $e, $time) { - $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 addError( + PHPUnit_Framework_Test $test, Exception $e, $time) + { + $this->addAssertion($test->test, false); + } - public function addIncompleteTest( - PHPUnit_Framework_Test $test, Exception $e, $time) { - $this->addAssertion($test->test, false); - } + public function addFailure( + PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) + { + $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( - 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) {} + public function addIncompleteTest( + PHPUnit_Framework_Test $test, Exception $e, $time) + { + $this->addAssertion($test->test, false); + } + + 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 { - public static function readFile($filename) { - $rval = @file_get_contents($filename); - if($rval === false) { - throw new Exception('File read error: ' . $filename); - } - return $rval; - } +class Util +{ + public static function readFile($filename) + { + $rval = @file_get_contents($filename); + if ($rval === false) { + throw new Exception('File read error: ' . $filename); + } - public static function readJson($filename) { - $rval = json_decode(self::readFile($filename)); - if($rval === null) { - throw new Exception('JSON parse error'); - } - return $rval; - } + return $rval; + } - public static function readNQuads($filename) { - return self::readFile($filename); - } + public static function readJson($filename) + { + $rval = json_decode(self::readFile($filename)); + if ($rval === null) { + throw new Exception('JSON parse error'); + } - public static function jsonldEncode($input) { - // newer PHP has a flag to avoid escaped '/' - if(defined('JSON_UNESCAPED_SLASHES')) { - $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; - } + return $rval; + } + + public static function readNQuads($filename) + { + return self::readFile($filename); + } + + public static function jsonldEncode($input) + { + // newer PHP has a flag to avoid escaped '/' + if (defined('JSON_UNESCAPED_SLASHES')) { + $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 -$SKIP_TESTS = array(); +$SKIP_TESTS = []; // root manifest directory $ROOT_MANIFEST_DIR; // parsed tests; keyed by type -$TESTS = array(); +$TESTS = []; // parsed command line options -$OPTIONS = array(); +$OPTIONS = []; // parse command line options global $argv; $args = $argv; $total = count($args); $start = false; -for($i = 0; $i < $total; ++$i) { - $arg = $args[$i]; - if(!$start) { - if(realpath($arg) === realpath(__FILE__)) { - $start = true; - } - continue; - } - if($arg[0] !== '-') { - break; - } - $i += 1; - $OPTIONS[$arg] = $args[$i]; +for ($i = 0; $i < $total; ++$i) { + $arg = $args[$i]; + if (!$start) { + if (realpath($arg) === realpath(__FILE__)) { + $start = true; + } + continue; + } + if ($arg[0] !== '-') { + break; + } + $i += 1; + $OPTIONS[$arg] = $args[$i]; } -if(!isset($OPTIONS['-d'])) { - $dvar = 'path to json-ld.org/test-suite'; - $evar = 'file to write EARL report to'; - echo "php-json-ld Tests\n"; - echo "Usage: phpunit test.php -d <$dvar> [-e <$evar>]\n\n"; - exit(0); + +if (!isset($OPTIONS['-d'])) { + $dvar = 'path to json-ld.org/test-suite'; + $evar = 'file to write EARL report to'; + echo "php-json-ld Tests\n"; + echo "Usage: phpunit test.php -d <$dvar> [-e <$evar>]\n\n"; + + exit(0); } // EARL Report $EARL = new EarlReport(); -if(isset($OPTIONS['-e'])) { - $EARL->filename = $OPTIONS['-e']; +if (isset($OPTIONS['-e'])) { + $EARL->filename = $OPTIONS['-e']; } // load root manifest $ROOT_MANIFEST_DIR = realpath($OPTIONS['-d']); $filename = join( - DIRECTORY_SEPARATOR, array($ROOT_MANIFEST_DIR, 'manifest.jsonld')); +DIRECTORY_SEPARATOR, [$ROOT_MANIFEST_DIR, 'manifest.jsonld']); $root_manifest = Util::readJson($filename); $manifest = new JsonLdManifest($root_manifest, $filename); $manifest->load($TESTS);