You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5593 lines
174 KiB
5593 lines
174 KiB
<?php |
|
/** |
|
* PHP implementation of the JSON-LD API. |
|
* Version: 0.1.0 |
|
* |
|
* @author Dave Longley |
|
* |
|
* BSD 3-Clause License |
|
* Copyright (c) 2011-2013 Digital Bazaar, Inc. |
|
* All rights reserved. |
|
* |
|
* Redistribution and use in source and binary forms, with or without |
|
* modification, are permitted provided that the following conditions are met: |
|
* |
|
* Redistributions of source code must retain the above copyright notice, |
|
* this list of conditions and the following disclaimer. |
|
* |
|
* Redistributions in binary form must reproduce the above copyright |
|
* notice, this list of conditions and the following disclaimer in the |
|
* documentation and/or other materials provided with the distribution. |
|
* |
|
* Neither the name of the Digital Bazaar, Inc. nor the names of its |
|
* contributors may be used to endorse or promote products derived from |
|
* this software without specific prior written permission. |
|
* |
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A |
|
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
|
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
*/ |
|
|
|
/** |
|
* Performs JSON-LD compaction. |
|
* |
|
* @param mixed $input the JSON-LD object to compact. |
|
* @param mixed $ctx the context to compact with. |
|
* @param assoc [$options] options to use: |
|
* [base] the base IRI to use. |
|
* [graph] true to always output a top-level graph (default: false). |
|
* [documentLoader(url)] the document loader. |
|
* |
|
* @return mixed the compacted JSON-LD output. |
|
*/ |
|
function jsonld_compact($input, $ctx, $options=array()) { |
|
$p = new JsonLdProcessor(); |
|
return $p->compact($input, $ctx, $options); |
|
} |
|
|
|
/** |
|
* 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. |
|
* [documentLoader(url)] the document loader. |
|
* |
|
* @return array the expanded JSON-LD output. |
|
*/ |
|
function jsonld_expand($input, $options=array()) { |
|
$p = new JsonLdProcessor(); |
|
return $p->expand($input, $options); |
|
} |
|
|
|
/** |
|
* Performs JSON-LD flattening. |
|
* |
|
* @param mixed $input the JSON-LD to flatten. |
|
* @param mixed $ctx the context to use to compact the flattened output, or |
|
* null. |
|
* @param [options] the options to use: |
|
* [base] the base IRI to use. |
|
* [documentLoader(url)] the document loader. |
|
* |
|
* @return mixed the flattened JSON-LD output. |
|
*/ |
|
function jsonld_flatten($input, $ctx, $options=array()) { |
|
$p = new JsonLdProcessor(); |
|
return $p->flatten($input, $ctx, $options); |
|
} |
|
|
|
/** |
|
* Performs JSON-LD framing. |
|
* |
|
* @param mixed $input the JSON-LD object to frame. |
|
* @param stdClass $frame the JSON-LD frame to use. |
|
* @param assoc [$options] the framing options. |
|
* [base] the base IRI to use. |
|
* [embed] default @embed flag (default: true). |
|
* [explicit] default @explicit flag (default: false). |
|
* [omitDefault] default @omitDefault flag (default: false). |
|
* [documentLoader(url)] the document loader. |
|
* |
|
* @return stdClass the framed JSON-LD output. |
|
*/ |
|
function jsonld_frame($input, $frame, $options=array()) { |
|
$p = new JsonLdProcessor(); |
|
return $p->frame($input, $frame, $options); |
|
} |
|
|
|
/** |
|
* Performs RDF dataset normalization on the given JSON-LD input. The output |
|
* is an RDF dataset unless the 'format' option is used. |
|
* |
|
* @param mixed $input the JSON-LD object to normalize. |
|
* @param assoc [$options] the options to use: |
|
* [base] the base IRI to use. |
|
* [format] the format if output is a string: |
|
* 'application/nquads' for N-Quads. |
|
* [documentLoader(url)] the document loader. |
|
* |
|
* @return mixed the normalized output. |
|
*/ |
|
function jsonld_normalize($input, $options=array()) { |
|
$p = new JsonLdProcessor(); |
|
return $p->normalize($input, $options); |
|
} |
|
|
|
/** |
|
* Converts an RDF dataset to JSON-LD. |
|
* |
|
* @param mixed $input 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 not an array: |
|
* '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. |
|
*/ |
|
function jsonld_from_rdf($input, $options=array()) { |
|
$p = new JsonLdProcessor(); |
|
return $p->fromRDF($input, $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. |
|
* [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). |
|
*/ |
|
function jsonld_to_rdf($input, $options=array()) { |
|
$p = new JsonLdProcessor(); |
|
return $p->toRDF($input, $options); |
|
} |
|
|
|
/** |
|
* Relabels all blank nodes in the given JSON-LD input. |
|
* |
|
* @param mixed input the JSON-LD input. |
|
*/ |
|
function jsonld_relabel_blank_nodes($input) { |
|
$p = new JsonLdProcessor(); |
|
return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input); |
|
} |
|
|
|
/** JSON-LD shared in-memory cache. */ |
|
global $jsonld_cache; |
|
$jsonld_cache = new stdClass(); |
|
|
|
/** The default active context cache. */ |
|
$jsonld_cache->activeCtx = new ActiveContextCache(); |
|
|
|
/** Stores the default JSON-LD document loader. */ |
|
global $jsonld_default_load_document; |
|
$jsonld_default_load_document = null; |
|
|
|
/** |
|
* Sets the default JSON-LD 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; |
|
} |
|
|
|
/** |
|
* Retrieves JSON-LD at the given URL. |
|
* |
|
* @param string $url the URL to retrieve. |
|
* |
|
* @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; |
|
} |
|
|
|
$remote_doc = $document_loader($url); |
|
if($remote_doc) { |
|
return $remote_doc->document; |
|
} |
|
return null; |
|
} |
|
|
|
/** |
|
* The default implementation to retrieve JSON-LD at the given URL. |
|
* |
|
* @param string $url the URL to to retrieve. |
|
* |
|
* @return stdClass the RemoteDocument object. |
|
*/ |
|
function jsonld_default_document_loader($url) { |
|
$redirects = array(); |
|
|
|
$opts = array( |
|
'http' => array( |
|
'method' => 'GET', |
|
'header' => |
|
"Accept: application/ld+json\r\n" . |
|
"User-Agent: JSON-LD PHP Client/1.0\r\n"), |
|
'https' => array( |
|
'verify_peer' => true, |
|
'method' => 'GET', |
|
'header' => |
|
"Accept: application/ld+json\r\n" . |
|
"User-Agent: JSON-LD PHP Client/1.0\r\n")); |
|
$stream = stream_context_create($opts); |
|
stream_context_set_params($stream, array('notification' => |
|
function($notification_code, $severity, $message) use (&$redirects) { |
|
switch($notification_code) { |
|
case STREAM_NOTIFY_REDIRECTED: |
|
$redirects[] = $message; |
|
break; |
|
}; |
|
})); |
|
$result = @file_get_contents($url, false, $stream); |
|
if($result === false) { |
|
throw new Exception("Could not GET url: '$url'"); |
|
} |
|
$redirs = count($redirects); |
|
if($redirs > 0) { |
|
$url = $redirects[$redirs - 1]; |
|
} |
|
// return RemoteDocument |
|
return (object)array( |
|
'contextUrl' => null, |
|
'document' => $result, |
|
'documentUrl' => $url); |
|
} |
|
|
|
/** |
|
* The default implementation to retrieve JSON-LD at the given secure URL. |
|
* |
|
* @param string $url the secure URL to to retrieve. |
|
* |
|
* @return stdClass the RemoteDocument object. |
|
*/ |
|
function jsonld_default_secure_document_loader($url) { |
|
if(strpos($url, 'https') !== 0) { |
|
throw new Exception("Could not GET url: '$url'; 'https' is required."); |
|
} |
|
|
|
$redirects = array(); |
|
|
|
// default JSON-LD https GET implementation |
|
$opts = array( |
|
'https' => array( |
|
'verify_peer' => true, |
|
'method' => "GET", |
|
'header' => |
|
"Accept: application/ld+json\r\n" . |
|
"User-Agent: JSON-LD PHP Client/1.0\r\n")); |
|
$stream = stream_context_create($opts); |
|
stream_context_set_params($stream, array('notification' => |
|
function($notification_code, $severity, $message) use (&$redirects) { |
|
switch($notification_code) { |
|
case STREAM_NOTIFY_REDIRECTED: |
|
$redirects[] = $message; |
|
break; |
|
}; |
|
})); |
|
$result = @file_get_contents($url, false, $stream); |
|
if($result === false) { |
|
throw new Exception("Could not GET url: '$url'"); |
|
} |
|
foreach($redirects as $redirect) { |
|
if(strpos($redirect, 'https') !== 0) { |
|
throw new Exception( |
|
"Could not GET redirected url: '$redirect'; 'https' is required."); |
|
} |
|
$url = $redirect; |
|
} |
|
// return RemoteDocument |
|
return (object)array( |
|
'contextUrl' => null, |
|
'document' => $result, |
|
'documentUrl' => $url); |
|
} |
|
|
|
/** Registered global RDF dataset parsers hashed by content-type. */ |
|
global $jsonld_rdf_parsers; |
|
$jsonld_rdf_parsers = new stdClass(); |
|
|
|
/** |
|
* Registers a global RDF dataset parser by content-type, for use with |
|
* jsonld_from_rdf. Global parsers will be used by JsonLdProcessors that do |
|
* not register their own parsers. |
|
* |
|
* @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). |
|
*/ |
|
function jsonld_register_rdf_parser($content_type, $parser) { |
|
global $jsonld_rdf_parsers; |
|
$jsonld_rdf_parsers->{$content_type} = $parser; |
|
} |
|
|
|
/** |
|
* Unregisters a global RDF dataset parser by content-type. |
|
* |
|
* @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}); |
|
} |
|
} |
|
|
|
/** |
|
* Parses a URL into its component parts. |
|
* |
|
* @param string $url the URL to parse. |
|
* |
|
* @return assoc the parsed URL. |
|
*/ |
|
function jsonld_parse_url($url) { |
|
if($url === null) { |
|
$url = ''; |
|
} |
|
|
|
$rval = parse_url($url); |
|
|
|
// malformed url |
|
if($rval === false) { |
|
$rval = array(); |
|
} |
|
|
|
$rval['href'] = $url; |
|
if(!isset($rval['scheme'])) { |
|
$rval['scheme'] = ''; |
|
$rval['protocol'] = ''; |
|
} |
|
else { |
|
$rval['protocol'] = $rval['scheme'] . ':'; |
|
} |
|
if(!isset($rval['host'])) { |
|
$rval['host'] = ''; |
|
} |
|
if(!isset($rval['path'])) { |
|
$rval['path'] = ''; |
|
} |
|
if(isset($rval['user']) || isset($rval['pass'])) { |
|
$rval['auth'] = ''; |
|
if(isset($rval['user'])) { |
|
$rval['auth'] = $rval['user']; |
|
} |
|
if(isset($rval['pass'])) { |
|
$rval['auth'] .= ":{$rval['pass']}"; |
|
} |
|
} |
|
// parse authority for unparsed relative network-path reference |
|
if(strpos($rval['href'], ':') === false && |
|
strpos($rval['href'], '//') === 0 && $rval['host'] === '') { |
|
// must parse authority from pathname |
|
$rval['path'] = substr($rval['path'], 2); |
|
$idx = strpos($rval['path'], '/'); |
|
if($idx === false) { |
|
$rval['authority'] = $rval['path']; |
|
$rval['path'] = ''; |
|
} |
|
else { |
|
$rval['authority'] = substr($rval['path'], 0, $idx); |
|
$rval['path'] = substr($rval['path'], $idx); |
|
} |
|
} |
|
else { |
|
$rval['authority'] = $rval['host']; |
|
if(isset($rval['port'])) { |
|
$rval['authority'] .= ":{$rval['port']}"; |
|
} |
|
if(isset($rval['auth'])) { |
|
$rval['authority'] = "{$rval['auth']}@{$rval['authority']}"; |
|
} |
|
} |
|
$rval['normalizedPath'] = jsonld_remove_dot_segments( |
|
$rval['path'], $rval['authority'] !== ''); |
|
|
|
return $rval; |
|
} |
|
|
|
/** |
|
* Removes dot segments from a URL path. |
|
* |
|
* @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 = ''; |
|
|
|
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); |
|
} |
|
// leading relative URL '..' |
|
else { |
|
$output[] = '..'; |
|
} |
|
continue; |
|
} |
|
$output[] = array_shift($input); |
|
} |
|
|
|
return $rval . implode('/', $output); |
|
} |
|
|
|
/** |
|
* Prepends a base IRI to the given relative IRI. |
|
* |
|
* @param mixed $base a string or the parsed base IRI. |
|
* @param string $iri the relative IRI. |
|
* |
|
* @return string the absolute IRI. |
|
*/ |
|
function jsonld_prepend_base($base, $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 given IRI |
|
$rel = jsonld_parse_url($iri); |
|
|
|
// start hierarchical part |
|
$hierPart = $base['protocol']; |
|
if($rel['authority']) { |
|
$hierPart .= "//{$rel['authority']}"; |
|
} |
|
else if($base['href'] !== '') { |
|
$hierPart .= "//{$base['authority']}"; |
|
} |
|
|
|
// per RFC3986 normalize |
|
|
|
// IRI represents an absolute path |
|
if(strpos($rel['path'], '/') === 0) { |
|
$path = $rel['path']; |
|
} |
|
else { |
|
$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']; |
|
} |
|
} |
|
|
|
// remove slashes and dots in path |
|
$path = jsonld_remove_dot_segments($path, $hierPart !== ''); |
|
|
|
// add query and hash |
|
if(isset($rel['query'])) { |
|
$path .= "?{$rel['query']}"; |
|
} |
|
if(isset($rel['fragment'])) { |
|
$path .= "#{$rel['fragment']}"; |
|
} |
|
|
|
$rval = $hierPart . $path; |
|
|
|
if($rval === '') { |
|
$rval = './'; |
|
} |
|
|
|
return $rval; |
|
} |
|
|
|
/** |
|
* Removes a base IRI from the given absolute IRI. |
|
* |
|
* @param mixed $base the base IRI. |
|
* @param string $iri the absolute IRI. |
|
* |
|
* @return string the relative IRI if relative to base, otherwise the absolute |
|
* IRI. |
|
*/ |
|
function jsonld_remove_base($base, $iri) { |
|
if(is_string($base)) { |
|
$base = jsonld_parse_url($base); |
|
} |
|
|
|
// establish base root |
|
$root = ''; |
|
if($base['href'] !== '') { |
|
$root .= "{$base['protocol']}//{$base['authority']}"; |
|
} |
|
// support network-path reference with empty base |
|
else if(strpos($iri, '//') === false) { |
|
$root .= '//'; |
|
} |
|
|
|
// 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 path segments that match |
|
$base_segments = explode('/', $base['normalizedPath']); |
|
$iri_segments = explode('/', $rel['normalizedPath']); |
|
while(count($base_segments) > 0 && count($iri_segments) > 0) { |
|
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 isn't a path (doesn't end in '/') |
|
// don't count empty first segment, it means base began with '/' |
|
if(substr($base['normalizedPath'], -1) !== '/' || |
|
$base_segments[0] === '') { |
|
array_pop($base_segments); |
|
} |
|
foreach($base_segments as $segment) { |
|
$rval .= '../'; |
|
} |
|
} |
|
|
|
// prepend remaining segments |
|
$rval .= implode('/', $iri_segments); |
|
|
|
// add query and hash |
|
if(isset($rel['query'])) { |
|
$rval .= "?{$rel['query']}"; |
|
} |
|
if(isset($rel['fragment'])) { |
|
$rval .= "#{$rel['fragment']}"; |
|
} |
|
|
|
if($rval === '') { |
|
$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) { |
|
if($ctx === null) { |
|
throw new JsonLdException( |
|
'The compaction context must not be null.', |
|
'jsonld.CompactError'); |
|
} |
|
|
|
// 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_document_loader')); |
|
|
|
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, $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, $e); |
|
} |
|
|
|
// do compaction |
|
$compacted = $this->_compact($active_ctx, null, $expanded, $options); |
|
|
|
if($options['compactArrays'] && |
|
!$options['graph'] && is_array($compacted)) { |
|
// simplify to a single item |
|
if(count($compacted) === 1) { |
|
$compacted = $compacted[0]; |
|
} |
|
// simplify to an empty object |
|
else if(count($compacted) === 0) { |
|
$compacted = new stdClass(); |
|
} |
|
} |
|
// always use array if graph option is on |
|
else if($options['graph']) { |
|
$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) { |
|
self::setdefaults($options, array( |
|
'base' => is_string($input) ? $input : '', |
|
'keepFreeFloatingNodes' => false, |
|
'documentLoader' => 'jsonld_default_document_loader')); |
|
|
|
// if input is a string, attempt to dereference remote document |
|
if(is_string($input)) { |
|
$remote_doc = $options['documentLoader']($input); |
|
} |
|
else { |
|
$remote_doc = (object)array( |
|
'contextUrl' => null, |
|
'documentUrl' => null, |
|
'document' => $input); |
|
} |
|
|
|
// build meta-object and retrieve all @context urls |
|
$input = (object)array( |
|
'document' => self::copy($input), |
|
'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, $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) { |
|
self::setdefaults($options, array( |
|
'base' => is_string($input) ? $input : '', |
|
'documentLoader' => 'jsonld_default_document_loader')); |
|
|
|
try { |
|
// expand input |
|
$expanded = $this->expand($input, $options); |
|
} |
|
catch(Exception $e) { |
|
throw new JsonLdException( |
|
'Could not expand input before flattening.', |
|
'jsonld.FlattenError', 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, $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 (default: true). |
|
* [explicit] default @explicit flag (default: false). |
|
* [omitDefault] default @omitDefault flag (default: false). |
|
* [documentLoader(url)] the document loader. |
|
* |
|
* @return stdClass the framed JSON-LD output. |
|
*/ |
|
public function frame($input, $frame, $options) { |
|
self::setdefaults($options, array( |
|
'base' => is_string($input) ? $input : '', |
|
'compactArrays' => true, |
|
'embed' => true, |
|
'explicit' => false, |
|
'omitDefault' => false, |
|
'documentLoader' => 'jsonld_default_document_loader')); |
|
|
|
// if frame is a string, attempt to dereference remote document |
|
if(is_string($frame)) { |
|
$remote_frame = $options['documentLoader']($frame); |
|
} |
|
else { |
|
$remote_frame = (object)array( |
|
'contextUrl' => null, |
|
'documentUrl' => null, |
|
'document' => $frame); |
|
} |
|
|
|
// 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, $e); |
|
} |
|
|
|
try { |
|
// expand frame |
|
$opts = self::copy($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, $e); |
|
} |
|
|
|
// do framing |
|
$framed = $this->_frame($expanded, $expanded_frame, $options); |
|
|
|
try { |
|
// compact result (force @graph option to true) |
|
$options['graph'] = true; |
|
$options['skipExpansion'] = true; |
|
$options['activeCtx'] = true; |
|
$result = $this->compact($framed, $ctx, $options); |
|
} |
|
catch(Exception $e) { |
|
throw new JsonLdException( |
|
'Could not compact framed output.', |
|
'jsonld.FrameError', null, $e); |
|
} |
|
|
|
$compacted = $result['compacted']; |
|
$active_ctx = $result['activeCtx']; |
|
|
|
// get graph alias |
|
$graph = $this->_compactIri($active_ctx, '@graph'); |
|
// remove @preserve from results |
|
$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. |
|
* [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) { |
|
self::setdefaults($options, array( |
|
'base' => is_string($input) ? $input : '', |
|
'documentLoader' => 'jsonld_default_document_loader')); |
|
|
|
try { |
|
// convert to RDF dataset then do normalization |
|
$opts = self::copy($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, $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', array('format' => $options['format'])); |
|
} |
|
if($this->rdfParsers !== null) { |
|
$callable = $this->rdfParsers->{$options['format']}; |
|
} |
|
else { |
|
$callable = $jsonld_rdf_parsers->{$options['format']}; |
|
} |
|
$dataset = $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) { |
|
self::setdefaults($options, array( |
|
'base' => is_string($input) ? $input : '', |
|
'produceGeneralizedRdf' => false, |
|
'documentLoader' => 'jsonld_default_document_loader')); |
|
|
|
try { |
|
// expand input |
|
$expanded = $this->expand($input, $options); |
|
} |
|
catch(JsonLdException $e) { |
|
throw new JsonLdException( |
|
'Could not expand input before serialization to RDF.', |
|
'jsonld.RdfError', $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}; |
|
$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', 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) { |
|
self::setdefaults($options, array( |
|
'base' => '', |
|
'documentLoader' => 'jsonld_default_document_loader')); |
|
|
|
// 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, $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; |
|
} |
|
} |
|
} |
|
// avoid matching the set of values with an array value parameter |
|
else if(!is_array($value)) { |
|
$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; |
|
} |
|
|
|
// return whole entry |
|
if($type === null) { |
|
$rval = $entry; |
|
} |
|
// return entry value for type |
|
else if(property_exists($entry, $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', 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); |
|
} |
|
// add triple if unique to its graph |
|
else { |
|
$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}>"; |
|
} |
|
// bnode normalization mode |
|
else if($bnode !== null) { |
|
$quad .= ($s->value === $bnode) ? '_:a' : '_:z'; |
|
} |
|
// bnode normal mode |
|
else { |
|
$quad .= $s->value; |
|
} |
|
$quad .= ' '; |
|
|
|
// predicate is an IRI |
|
if($p->type === 'IRI') { |
|
$quad .= "<{$p->value}>"; |
|
} |
|
// FIXME: TBD what to do with bnode predicates during normalization |
|
// bnode normalization mode |
|
else if($bnode !== null) { |
|
$quad .= '_:p'; |
|
} |
|
// bnode normal mode |
|
else { |
|
$quad .= $p->value; |
|
} |
|
$quad .= ' '; |
|
|
|
// object is IRI, bnode, or literal |
|
if($o->type === 'IRI') { |
|
$quad .= "<{$o->value}>"; |
|
} |
|
else if($o->type === 'blank node') { |
|
// normalization mode |
|
if($bnode !== null) { |
|
$quad .= ($o->value === $bnode) ? '_:a' : '_:z'; |
|
} |
|
// normal mode |
|
else { |
|
$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)); |
|
} |
|
else { |
|
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)) { |
|
// do value compaction on @values and subject references |
|
if(self::_isValue($element) || self::_isSubjectReference($element)) { |
|
return $this->_compactValue($active_ctx, $active_property, $element); |
|
} |
|
|
|
// FIXME: avoid misuse of active property as an expanded property? |
|
$inside_reverse = ($active_property === '@reverse'); |
|
|
|
// process element keys in order |
|
$keys = array_keys((array)$element); |
|
sort($keys); |
|
$rval = new stdClass(); |
|
foreach($keys as $expanded_property) { |
|
$expanded_value = $element->{$expanded_property}; |
|
|
|
// compact @id and @type(s) |
|
if($expanded_property === '@id' || $expanded_property === '@type') { |
|
// compact single @id |
|
if(is_string($expanded_value)) { |
|
$compacted_value = $this->_compactIri( |
|
$active_ctx, $expanded_value, null, |
|
array('vocab' => ($expanded_property === '@type'))); |
|
} |
|
// expanded value must be a @type array |
|
else { |
|
$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; |
|
} |
|
|
|
// 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'}; |
|
} |
|
} |
|
// can't use @list container for more than 1 list |
|
else if(property_exists($rval, $item_active_property)) { |
|
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'); |
|
} |
|
} |
|
|
|
// 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(); |
|
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'); |
|
} |
|
// drop null values |
|
else if($e !== null) { |
|
if(is_array($e)) { |
|
$rval = array_merge($rval, $e); |
|
} |
|
else { |
|
$rval[] = $e; |
|
} |
|
} |
|
} |
|
return $rval; |
|
} |
|
|
|
// recursively expand object |
|
if(is_object($element)) { |
|
// 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; |
|
} |
|
|
|
// get term definition for key |
|
if(property_exists($active_ctx->mappings, $key)) { |
|
$mapping = $active_ctx->mappings->{$key}; |
|
} |
|
else { |
|
$mapping = null; |
|
} |
|
|
|
// 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) && |
|
$expanded_active_property === '@reverse') { |
|
throw new JsonLdException( |
|
'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' . |
|
'property.', |
|
'jsonld.SyntaxError', array('value' => $value)); |
|
} |
|
|
|
// syntax error if @id is not a string |
|
if($expanded_property === '@id' && !is_string($value)) { |
|
throw new JsonLdException( |
|
'Invalid JSON-LD syntax; "@id" value must a string.', |
|
'jsonld.SyntaxError', 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; "@value" value must not be an ' . |
|
'object or an array.', |
|
'jsonld.SyntaxError', 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', array('value' => $value)); |
|
} |
|
|
|
// @language must be a string |
|
if($expanded_property === '@language' && !is_string($value)) { |
|
throw new JsonLdException( |
|
'Invalid JSON-LD syntax; "@language" value must not be a string.', |
|
'jsonld.SyntaxError', 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', 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', 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', |
|
array('value' => $expanded_value)); |
|
} |
|
self::addValue( |
|
$reverse_map, $property, $item, |
|
array('propertyIsArray' => true)); |
|
} |
|
} |
|
|
|
continue; |
|
} |
|
|
|
$container = self::getContextValue($active_ctx, $key, '@container'); |
|
|
|
// handle language map container (skip if value is not an object) |
|
if($container === '@language' && is_object($value)) { |
|
$expanded_value = $this->_expandLanguageMap($value); |
|
} |
|
// handle index container (skip if value is not an object) |
|
else if($container === '@index' && is_object($value)) { |
|
$expand_index_map = function($active_property) use ( |
|
$active_ctx, $options, $value) { |
|
$rval = array(); |
|
$keys = array_keys((array)$value); |
|
sort($keys); |
|
foreach($keys as $key) { |
|
$val = $value->{$key}; |
|
$val = self::arrayify($val); |
|
$val = $this->_expand( |
|
$active_ctx, $active_property, $val, $options, false); |
|
foreach($val as $item) { |
|
if(!property_exists($item, '@index')) { |
|
$item->{'@index'} = $key; |
|
} |
|
$rval[] = $item; |
|
} |
|
} |
|
return $rval; |
|
}; |
|
$expanded_value = $expand_index_map($key); |
|
} |
|
else { |
|
// recurse into @list or @set keeping the active property |
|
$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'); |
|
} |
|
} |
|
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) { |
|
$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', 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', 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', array('element' => $rval)); |
|
} |
|
// drop null @values |
|
if($rval->{'@value'} === null) { |
|
$rval = null; |
|
} |
|
// drop @language if @value isn't a string |
|
else if(property_exists($rval, '@language') && |
|
!is_string($rval->{'@value'})) { |
|
unset($rval->{'@language'}); |
|
} |
|
} |
|
// convert @type to an array |
|
else if(property_exists($rval, '@type') && !is_array($rval->{'@type'})) { |
|
$rval->{'@type'} = array($rval->{'@type'}); |
|
} |
|
// handle @set and @list |
|
else if(property_exists($rval, '@set') || |
|
property_exists($rval, '@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', array('element' => $rval)); |
|
} |
|
// optimize away @set |
|
if(property_exists($rval, '@set')) { |
|
$rval = $rval->{'@set'}; |
|
$keys = array_keys((array)$rval); |
|
$count = count($keys); |
|
} |
|
} |
|
// drop objects with only @language |
|
else if($count === 1 && property_exists($rval, '@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 |
|
if($count === 0 || property_exists($rval, '@value')) { |
|
$rval = null; |
|
} |
|
else { |
|
// drop subjects that generate no triples |
|
$has_triples = false; |
|
$ignore = array('@graph', '@type'); |
|
foreach($keys as $key) { |
|
if(!self::_isKeyword($key) || in_array($key, $ignore)) { |
|
$has_triples = true; |
|
break; |
|
} |
|
} |
|
if(!$has_triples) { |
|
$rval = null; |
|
} |
|
} |
|
} |
|
|
|
return $rval; |
|
} |
|
|
|
// drop top-level 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); |
|
} |
|
|
|
/** |
|
* 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())); |
|
|
|
// 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 |
|
|