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.
php-json-ld/jsonld.php

5566 lines
173 KiB

<?php
/**
* PHP implementation of the JSON-LD API.
* Version: 0.0.39
*
* @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).
* [loadDocument(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.
* [loadDocument(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.
* [loadDocument(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).
* [loadDocument(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.
* [loadDocument(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.
* [loadDocument(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_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.
* [loadDocument(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.
* [loadDocument(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.
* [loadDocument(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).
* [loadDocument(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.
* [loadDocument(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']);
}
$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.
* [loadDocument(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 : '',
'documentLoader' => 'jsonld_default_document_loader'));
try {
// expand input
$expanded = $this->expand($input, $options);
}
catch(JsonLdException $e) {
throw new JsonLdException(
'Could not expand input before conversion 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);
}
$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:
* [loadDocument(url)] the document loader.
*
* @return stdClass the new active context.
*/
public function processContext($active_ctx, $local_ctx, $options) {
self::setdefaults($options, array(
'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) {
$subject->{'@graph'}[] = $node_map->{$id};
}
}
// produce flattened output
$flattened = array();
$keys = array_keys((array)$default_graph);
sort($keys);
foreach($keys as $key) {
$flattened[] = $default_graph->{$key};
}
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
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', 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 conversion options.
*
* @return array the JSON-LD output.
*/
protected function _fromRDF($dataset, $options) {
$default_graph = new stdClass();
$graph_map = (object)array('@default' => $default_graph);
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 && $o->value !== self::RDF_NIL &&
!property_exists($node_map, $o->value)) {
$node_map->{$o->value} = (object)array('@id' => $o->value);
}
if($p === self::RDF_TYPE && $object_is_id) {
self::addValue(
$node, '@type', $o->value, array('propertyIsArray' => true));
continue;
}
if($object_is_id &&
$o->value === self::RDF_NIL && $p !== self::RDF_REST) {
// empty list detected
$value = (object)array('@list' => array());
}
else {
$value = self::_RDFToObject($o, $options['useNativeTypes']);
}
self::addValue($node, $p, $value, array('propertyIsArray' => true));
// object may be the head of an RDF list but we can't know easily
// until all triples are read
if($o->type === 'blank node' &&
!($p === self::RDF_FIRST || $p === self::RDF_REST)) {
$object = $node_map->{$o->value};
if(!property_exists($object, 'listHeadFor')) {
$object->listHeadFor = $value;
}
// can't be a list head if referenced more than once
else {
$object->listHeadFor = null;
}
}
}
}
// convert linked lists to @list arrays
foreach($graph_map as $name => $graph_object) {
foreach($graph_object as $subject => $node) {
// if subject not in graph_object, it has been removed as it was
// part of an RDF list, continue
if(!property_exists($graph_object, $subject)) {
continue;
}
// if value is not an object, it can't be a list head, continue
$value = (property_exists($node, 'listHeadFor') ?
$node->listHeadFor : null);
if(!is_object($value)) {
continue;
}
$list = array();
$eliminated_nodes = new stdClass();
while($subject !== self::RDF_NIL && $list !== null) {
// ensure node is a valid list node; node must:
// 1. Be a blank node
// 2. Have no keys other than: @id, listHeadFor, rdf:first, rdf:rest.
// 3. Have an array for rdf:first that has 1 item.
// 4. Have an array for rdf:rest that has 1 object with @id.
// 5. Not already be in a list (it is in the eliminated nodes map).
$node_key_count = (is_object($node) ?
count(array_keys((array)$node)) : 0);
if(!(is_object($node) && strpos($node->{'@id'}, '_:') === 0 &&
($node_key_count === 3 || ($node_key_count === 4 &&
property_exists($node, 'listHeadFor'))) &&
property_exists($node, self::RDF_FIRST) &&
property_exists($node, self::RDF_REST) &&
is_array($node->{self::RDF_FIRST}) &&
count($node->{self::RDF_FIRST}) === 1 &&
is_array($node->{self::RDF_REST}) &&
count($node->{self::RDF_REST}) === 1 &&
is_object($node->{self::RDF_REST}[0]) &&
property_exists($node->{self::RDF_REST}[0], '@id') &&
!property_exists($eliminated_nodes, $subject))) {
$list = null;
break;
}
$list[] = $node->{self::RDF_FIRST}[0];
$eliminated_nodes->{$node->{'@id'}} = true;
$subject = $node->{self::RDF_REST}[0]->{'@id'};
$node = (property_exists($graph_object, $subject) ?
$graph_object->{$subject} : null);
}
// bad list detected, skip it
if($list === null) {
continue;
}
unset($value->{'@id'});
$value->{'@list'} = $list;
foreach($eliminated_nodes as $id => $b) {
unset($graph_object->{$id});
}
}
}
$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_};
unset($node_->listHeadFor);
$node->{'@graph'}[] = $node_;
}
}
unset($node->listHeadFor);
$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
$rval = $active_ctx;
$must_clone = true;
foreach($ctxs as $ctx) {
// reset to initial context
if($ctx === null) {
$rval = $this->_getInitialContext($options);
$must_clone = false;
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', 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 = $cached;
$must_clone = true;
continue;
}
}
// clone context, if required, before updating
if($must_clone) {
$rval = self::_cloneActiveContext($rval);
$must_clone = false;
}
// 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', 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', array('context' => $ctx));
}
$rval->{'@base'} = jsonld_parse_url($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', array('context' => $ctx));
}
else