New port of jsonld.js.

This commit is contained in:
Dave Longley 2012-04-23 12:57:06 -04:00
commit cdce63a99c
2 changed files with 3545 additions and 3114 deletions

View file

@ -12,313 +12,275 @@ require_once('jsonld.php');
$isCli = defined('STDIN');
$eol = $isCli ? "\n" : '<br/>';
function error_handler($errno, $errstr, $errfile, $errline)
{
global $eol;
echo "$eol$errstr$eol";
array_walk(
debug_backtrace(),
create_function(
'$a,$b',
'echo "{$a[\'function\']}()' .
'(".basename($a[\'file\']).":{$a[\'line\']}); ' . $eol . '";'));
throw new Exception();
return false;
function error_handler($errno, $errstr, $errfile, $errline) {
global $eol;
echo "$eol$errstr$eol";
array_walk(
debug_backtrace(),
create_function(
'$a,$b',
'echo "{$a[\'function\']}()' .
'(".basename($a[\'file\']).":{$a[\'line\']}); ' . $eol . '";'));
throw new Exception();
return false;
}
if(!$isCli)
{
set_error_handler('error_handler');
if(!$isCli) {
set_error_handler('error_handler');
}
function _sortKeys($obj)
{
$rval;
if($obj === null)
{
$rval = null;
}
else if(is_array($obj))
{
$rval = array();
foreach($obj as $o)
{
$rval[] = _sortKeys($o);
function deep_compare($expect, $result) {
if(is_array($expect)) {
if(!is_array($result)) {
return false;
}
if(count($expect) !== count($result)) {
return false;
}
foreach($expect as $i => $v) {
if(!deep_compare($v, $result[$i])) {
return false;
}
}
else if(is_object($obj))
{
$rval = new stdClass();
$keys = array_keys((array)$obj);
sort($keys);
foreach($keys as $key)
{
$rval->$key = _sortKeys($obj->$key);
}
return true;
}
if(is_object($expect)) {
if(!is_object($result)) {
return false;
}
if(count(get_object_vars($expect)) !== count(get_object_vars($result))) {
return false;
}
foreach($expect as $k => $v) {
if(!deep_compare($v, $result->{$k})) {
return false;
}
}
else
{
$rval = $obj;
}
}
}
return $rval;
}
function _stringifySorted($obj, $indent)
{
/*
$flags = JSON_UNESCAPED_SLASHES;
if($indent)
{
$flags |= JSON_PRETTY_PRINT;
}*/
return str_replace('\\/', '/', json_encode(_sortKeys($obj)));//, $flags);
return $expect === $result;
}
/**
* Reads test JSON files.
*
* @param file the file to read.
* @param filepath the test filepath.
* @param string $file the file to read.
* @param string $filepath the test filepath.
*
* @return the read JSON.
* @return string the read JSON.
*/
function _readTestJson($file, $filepath)
{
$rval;
global $eol;
function read_test_json($file, $filepath) {
global $eol;
try
{
$file = $filepath . '/' . $file;
$rval = json_decode(file_get_contents($file));
}
catch(Exception $e)
{
echo "Exception while parsing file: '$file'$eol";
throw $e;
}
return $rval;
try {
$file = $filepath . '/' . $file;
return json_decode(file_get_contents($file));
}
catch(Exception $e) {
echo "Exception while parsing file: '$file'$eol";
throw $e;
}
}
class TestRunner
{
public function __construct()
{
// set up groups, add root group
$this->groups = array();
$this->group('');
}
class TestRunner {
public function __construct() {
// set up groups, add root group
$this->groups = array();
$this->group('');
public function group($name)
{
$group = new stdClass();
$group->name = $name;
$group->tests = array();
$group->count = 1;
$this->groups[] = $group;
}
$this->passed = 0;
$this->failed = 0;
$this->total = 0;
}
public function ungroup()
{
array_pop($this->groups);
}
public function group($name) {
$this->groups[] = (object)array(
'name' => $name,
'tests' => array(),
'count' => 1);
}
public function test($name)
{
$this->groups[count($this->groups) - 1]->tests[] = $name;
public function ungroup() {
array_pop($this->groups);
}
$line = '';
foreach($this->groups as $g)
{
$line .= ($line === '') ? $g->name : ('/' . $g->name);
public function test($name) {
$this->groups[count($this->groups) - 1]->tests[] = $name;
$this->total += 1;
$line = '';
foreach($this->groups as $g) {
$line .= ($line === '') ? $g->name : ('/' . $g->name);
}
$g = $this->groups[count($this->groups) - 1];
if($g->name !== '') {
$count = '' . $g->count;
$end = 4 - strlen($count);
for($i = 0; $i < $end; ++$i) {
$count = '0' . $count;
}
$line .= ' ' . $count;
$g->count += 1;
}
$line .= '/' . array_pop($g->tests) . '... ';
echo $line;
}
$g = $this->groups[count($this->groups) - 1];
if($g->name !== '')
{
$count = '' . $g->count;
$end = 4 - strlen($count);
for($i = 0; $i < $end; ++$i)
{
$count = '0' . $count;
}
$line .= ' ' . $count;
$g->count += 1;
public function check($test, $expect, $result) {
global $eol;
if(strpos($test->{'@type'}, 'NormalizeTest') !== false) {
$pass = JsonLdProcessor::compareNormalized($expect, $result);
}
else {
$pass = deep_compare($expect, $result);
}
if($pass) {
$this->passed += 1;
echo "PASS$eol";
}
else {
$this->failed += 1;
echo "FAIL$eol";
echo 'Expect: ' . print_r($expect, true) . $eol;
echo 'Result: ' . print_r($result, true) . $eol;
/*
$flags = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT;
echo 'JSON Expect: ' .
json_encode(json_decode(expect, $flags)) . $eol;
echo 'JSON Result: ' .
json_encode(json_decode(result, $flags)) . $eol;
*/
// FIXME: remove me
throw new Exception('FAIL');
}
}
public function load($filepath) {
global $eol;
$manifests = array();
// get full path
$filepath = realpath($filepath);
echo "Reading manifest files from: '$filepath'$eol";
// read each test file from the directory
$files = array();
$handle = opendir($filepath);
if($handle) {
while(($file = readdir($handle)) !== false) {
if($file !== '..' and $file !== '.')
{
$files[] = $filepath . '/' . $file;
}
}
closedir($handle);
}
else {
throw new Exception('Could not open directory.');
}
foreach($files as $file) {
$info = pathinfo($file);
// FIXME: hackish, manifests are now JSON-LD
if(strstr($info['basename'], 'manifest') !== false &&
$info['extension'] == 'jsonld') {
echo "Reading manifest file: '$file'$eol";
try {
$manifest = json_decode(file_get_contents($file));
}
catch(Exception $e) {
echo "Exception while parsing file: '$file'$eol";
throw $e;
}
$manifest->filepath = $filepath;
$manifests[] = $manifest;
}
$line .= '/' . array_pop($g->tests) . '... ';
echo $line;
}
}
public function check($expect, $result, $indent=false)
{
global $eol;
echo count($manifests) . " manifest file(s) read.$eol";
return $manifests;
}
// sort and use given indent level
$expect = _stringifySorted($expect, $indent);
$result = _stringifySorted($result, $indent);
public function run($manifests) {
/* Manifest format: {
name: <optional manifest name>,
sequence: [{
'name': <test name>,
'@type': ["test:TestCase", "jld:<type of test>"],
'input': <input file for test>,
'context': <context file for add context test type>,
'frame': <frame file for frame test type>,
'expect': <expected result file>,
}]
}
*/
global $eol;
foreach($manifests as $manifest) {
$this->group($manifest->name);
$filepath = $manifest->filepath;
foreach($manifest->sequence as $test) {
// read test input files
$type = $test->{'@type'};
$options = array(
'base' => 'http://json-ld.org/test-suite/tests/' . $test->input);
if(in_array('jld:NormalizeTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_normalize($input, $options);
}
else if(in_array('jld:ExpandTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_expand($input, $options);
}
else if(in_array('jld:CompactTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->context = read_test_json($test->context, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_compact($input, $test->context, $options);
}
else if(in_array('jld:FrameTest', $type)) {
$this->test($test->name);
$input = read_test_json($test->input, $filepath);
$test->frame = read_test_json($test->frame, $filepath);
$test->expect = read_test_json($test->expect, $filepath);
$result = jsonld_frame($input, $test->frame, $options);
}
else {
echo "Skipping test \"{$test->name}\" of type: " .
json_encode($type) . $eol;
continue;
}
$fail = false;
if($expect === $result)
{
$line = 'PASS';
}
else
{
$line = 'FAIL';
$fail = true;
// check results
$this->check($test, $test->expect, $result);
}
}
}
}
echo "$line$eol";
if($fail)
{
echo 'Expect: ' . print_r($expect, true) . $eol;
echo 'Result: ' . print_r($result, true) . $eol;
/*
$flags = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT;
echo 'Legible Expect: ' .
json_encode(json_decode(expect, $flags)) . $eol;
echo 'Legible Result: ' .
json_encode(json_decode(result, $flags)) . $eol;
*/
// FIXME: remove me
throw new Exception('FAIL');
}
}
public function load($filepath)
{
global $eol;
$manifests = array();
// get full path
$filepath = realpath($filepath);
echo "Reading manifest files from: '$filepath'$eol";
// read each test file from the directory
$files = array();
$handle = opendir($filepath);
if($handle)
{
while(($file = readdir($handle)) !== false)
{
if($file !== '..' and $file !== '.')
{
$files[] = $filepath . '/' . $file;
}
}
closedir($handle);
}
else
{
throw new Exception('Could not open directory.');
}
foreach($files as $file)
{
$info = pathinfo($file);
// FIXME: hackish, manifests are now JSON-LD
if(strstr($info['basename'], 'manifest') !== false and
$info['extension'] == 'jsonld')
{
echo "Reading manifest file: '$file'$eol";
try
{
$manifest = json_decode(file_get_contents($file));
}
catch(Exception $e)
{
echo "Exception while parsing file: '$file'$eol";
throw $e;
}
$manifest->filepath = $filepath;
$manifests[] = $manifest;
}
}
echo count($manifests) . " manifest file(s) read.$eol";
return $manifests;
}
public function run($manifests)
{
/* Manifest format:
{
name: <optional manifest name>,
sequence: [{
'name': <test name>,
'@type': ["test:TestCase", "jld:<type of test>"],
'input': <input file for test>,
'context': <context file for add context test type>,
'frame': <frame file for frame test type>,
'expect': <expected result file>,
}]
}
*/
global $eol;
foreach($manifests as $manifest)
{
$this->group($manifest->name);
$filepath = $manifest->filepath;
foreach($manifest->sequence as $test)
{
// read test input files
$indent = 2;
$type = $test->{'@type'};
if(in_array('jld:NormalizeTest', $type))
{
$indent = 0;
$input = _readTestJson($test->input, $filepath);
$test->expect = _readTestJson($test->expect, $filepath);
$result = jsonld_normalize($input);
}
else if(in_array('jld:ExpandTest', $type))
{
$input = _readTestJson($test->input, $filepath);
$test->expect = _readTestJson($test->expect, $filepath);
$result = jsonld_expand($input);
}
else if(in_array('jld:CompactTest', $type))
{
$input = _readTestJson($test->input, $filepath);
$test->context = _readTestJson($test->context, $filepath);
$test->expect = _readTestJson($test->expect, $filepath);
$result = jsonld_compact($test->context->{'@context'}, $input);
}
else if(in_array('jld:FrameTest', $type))
{
$input = _readTestJson($test->input, $filepath);
$test->frame = _readTestJson($test->frame, $filepath);
$test->expect = _readTestJson($test->expect, $filepath);
$result = jsonld_frame($input, $test->frame);
}
else
{
echo 'Skipping test "' . $test->name . '" of type: ' .
json_encode($type) . $eol;
continue;
}
// check results (only indent output on non-normalize tests)
$this->test($test->name);
$this->check($test->expect, $result, $indent);
}
}
}
// get command line options
$options = getopt('d:');
if($options === false || !array_key_exists('d', $options)) {
$var = 'path to json-ld.org/test-suite/tests';
echo "Usage: php jsonld-tests.php -d <$var>$eol";
exit(0);
}
// load and run tests
$tr = new TestRunner();
$tr->group('JSON-LD');
$tr->run($tr->load('tests'));
$tr->run($tr->load($options['d']));
$tr->ungroup();
echo "All tests complete.$eol";

6149
jsonld.php
View file

@ -1,2933 +1,3402 @@
<?php
/**
* PHP implementation of JSON-LD.
* PHP implementation of the JSON-LD API.
*
* @author Dave Longley
*
* Copyright (c) 2011-2012 Digital Bazaar, Inc. All rights reserved.
* BSD 3-Clause License
* Copyright (c) 2011-2012 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.
*/
define('JSONLD_XSD', 'http://www.w3.org/2001/XMLSchema#');
define('JSONLD_XSD_BOOLEAN', JSONLD_XSD . 'boolean');
define('JSONLD_XSD_DOUBLE', JSONLD_XSD . 'double');
define('JSONLD_XSD_INTEGER', JSONLD_XSD . 'integer');
/**
* Normalizes a JSON-LD object.
* Performs JSON-LD compaction.
*
* @param input the JSON-LD object to normalize.
* @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.
* [strict] use strict mode (default: true).
* [optimize] true to optimize the compaction (default: false).
* [graph] true to always output a top-level graph (default: false).
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return the normalized JSON-LD object.
* @return mixed the compacted JSON-LD output.
*/
function jsonld_normalize($input)
{
$p = new JsonLdProcessor();
return $p->normalize($input);
function jsonld_compact($input, $ctx, $options=array()) {
$p = new JsonLdProcessor();
return $p->compact($input, $ctx, $options);
}
/**
* Removes the context from a JSON-LD object, expanding it to full-form.
* Performs JSON-LD expansion.
*
* @param input the JSON-LD object to remove the context from.
* @param mixed $input the JSON-LD object to expand.
* @param assoc[$options] the options to use:
* [base] the base IRI to use.
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return the context-neutral JSON-LD object.
* @return array the expanded JSON-LD output.
*/
function jsonld_expand($input)
{
$p = new JsonLdProcessor();
return $p->expand(new stdClass(), null, $input);
function jsonld_expand($input, $options=array()) {
$p = new JsonLdProcessor();
return $p->expand($input, $options);
}
/**
* Expands the given JSON-LD object and then compacts it using the
* given context.
* Performs JSON-LD framing.
*
* @param ctx the new context to use.
* @param input the input JSON-LD object.
* @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).
* [optimize] optimize when compacting (default: false).
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return the output JSON-LD object.
* @return stdClass the framed JSON-LD output.
*/
function jsonld_compact($ctx, $input)
{
$rval = null;
// TODO: should context simplification be optional? (ie: remove context
// entries that are not used in the output)
if($input !== null)
{
// fully expand input
$input = jsonld_expand($input);
// merge context if it is an array
if(is_array($ctx))
{
$ctx = jsonld_merge_contexts(new stdClass, $ctx);
}
// setup output context
$ctxOut = new stdClass();
// compact
$p = new JsonLdProcessor();
$rval = $out = $p->compact(_clone($ctx), null, $input, $ctxOut);
// add context if used
if(count(array_keys((array)$ctxOut)) > 0)
{
$rval = new stdClass();
$rval->{'@context'} = $ctxOut;
if(is_array($out))
{
$rval->{_getKeywords($ctxOut)->{'@id'}} = $out;
}
else
{
foreach($out as $k => $v)
{
$rval->{$k} = $v;
}
}
}
}
return $rval;
function jsonld_frame($input, $frame, $options=array()) {
$p = new JsonLdProcessor();
return $p->frame($input, $frame, $options);
}
/**
* Merges one context with another.
* Performs JSON-LD normalization.
*
* @param ctx1 the context to overwrite/append to.
* @param ctx2 the new context to merge onto ctx1.
* @param mixed $input the JSON-LD object to normalize.
* @param assoc [$options] the options to use:
* [base] the base IRI to use.
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return the merged context.
* @return array the normalized JSON-LD output.
*/
function jsonld_merge_contexts($ctx1, $ctx2)
{
// merge first context if it is an array
if(is_array($ctx1))
{
$ctx1 = jsonld_merge_contexts($ctx1, $ctx2);
}
// copy context to merged output
$merged = _clone($ctx1);
if(is_array($ctx2))
{
// merge array of contexts in order
foreach($ctx2 as $ctx)
{
$merged = jsonld_merge_contexts($merged, $ctx);
}
}
else
{
// if the new context contains any IRIs that are in the merged context,
// remove them from the merged context, they will be overwritten
foreach($ctx2 as $key => $value)
{
// ignore special keys starting with '@'
if(strpos($key, '@') !== 0)
{
foreach($merged as $mkey => $mvalue)
{
if($mvalue === $value)
{
// FIXME: update related coerce rules
unset($merged->$mkey);
break;
}
}
}
}
// merge contexts
foreach($ctx2 as $key => $value)
{
$merged->$key = _clone($value);
}
}
return $merged;
function jsonld_normalize($input, $options=array()) {
$p = new JsonLdProcessor();
return $p->normalize($input, $options);
}
/**
* Expands a term into an absolute IRI. The term may be a regular term, a
* prefix, a relative IRI, or an absolute IRI. In any case, the associated
* absolute IRI will be returned.
* Outputs the RDF statements found in the given JSON-LD object.
*
* @param ctx the context to use.
* @param term the term to expand.
* @param mixed $input the JSON-LD object.
* @param assoc [$options] the options to use:
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return the expanded term as an absolute IRI.
* @return array all RDF statements in the JSON-LD object.
*/
function jsonld_expand_term($ctx, $term)
{
return _expandTerm($ctx, $term);
function jsonld_to_rdf($input, $options=array()) {
$p = new JsonLdProcessor();
return $p->toRdf($input, $options);
}
/**
* Compacts an IRI into a term or prefix if it can be. IRIs will not be
* compacted to relative IRIs if they match the given context's default
* vocabulary.
* Processes a local context, resolving any URLs as necessary, and returns a
* new active context.
*
* @param ctx the context to use.
* @param iri the IRI to compact.
* @param active_ctx the current active context.
* @param local_ctx the local context to process.
* @param [options] the options to use:
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return the compacted IRI as a term or prefix or the original IRI.
* @return the new active context.
*/
function jsonld_compact_iri($ctx, $iri)
{
return _compactIri($ctx, $iri, null);
}
/**
* Frames JSON-LD input.
*
* @param input the JSON-LD input.
* @param frame the frame to use.
* @param options framing options to use.
*
* @return the framed output.
*/
function jsonld_frame($input, $frame, $options=null)
{
$rval;
// normalize input
$input = jsonld_normalize($input);
// save frame context
$ctx = null;
if(is_object($frame) and property_exists($frame, '@context'))
{
$ctx = _clone($frame->{'@context'});
// remove context from frame
$frame = jsonld_expand($frame);
}
else if(is_array($frame))
{
// save first context in the array
if(count($frame) > 0 and property_exists($frame[0], '@context'))
{
$ctx = _clone($frame[0]->{'@context'});
}
// expand all elements in the array
$tmp = array();
foreach($frame as $f)
{
$tmp[] = jsonld_expand($f);
}
$frame = $tmp;
}
// create framing options
// TODO: merge in options from function parameter
$options = new stdClass();
$options->defaults = new stdClass();
$options->defaults->embedOn = true;
$options->defaults->explicitOn = false;
$options->defaults->omitDefaultOn = false;
// build map of all subjects
$subjects = new stdClass();
foreach($input as $i)
{
$subjects->{$i->{'@id'}} = $i;
}
// frame input
$rval = _frame(
$subjects, $input, $frame, new stdClass(), false, null, null, $options);
// apply context
if($ctx !== null and $rval !== null)
{
// preserve top-level array by compacting individual entries
if(is_array($rval))
{
$tmp = $rval;
$rval = array();
foreach($tmp as $value)
{
$rval[] = jsonld_compact($ctx, $value);
}
}
else
{
$rval = jsonld_compact($ctx, $rval);
}
}
return $rval;
}
/**
* Resolves external @context URLs. Every @context URL in the given JSON-LD
* object is resolved using the given URL-resolver function. Once all of
* the @contexts have been resolved, the method will return. If an error
* is encountered, an exception will be thrown.
*
* @param input the JSON-LD input object (or array).
* @param resolver the resolver method that takes a URL and returns a JSON-LD
* serialized @context or throws an exception.
*
* @return the fully-resolved JSON-LD output (object or array).
*/
function jsonld_resolve($input, $resolver)
{
// find all @context URLs
$urls = new ArrayObject();
_findUrls($input, $urls, false);
// resolve all URLs
foreach($urls as $url => $value)
{
$result = call_user_func($resolver, $url);
if(!is_string($result))
{
// already deserialized
$urls[$url] = $result->{'@context'};
}
else
{
// deserialize JSON
$tmp = json_decode($result);
if($tmp === null)
{
throw new Exception(
"Could not resolve @context URL ('$url'), " .
'malformed JSON detected.');
}
$urls[$url] = $tmp->{'@context'};
}
}
// replace @context URLs in input
_findUrls($input, $urls, true);
return $input;
}
/**
* Finds all of the @context URLs in the given input and replaces them
* if requested by their associated values in the given URL map.
*
* @param input the JSON-LD input object.
* @param urls the URLs ArrayObject.
* @param replace true to replace, false not to.
*/
function _findUrls($input, $urls, $replace)
{
if(is_array($input))
{
foreach($input as $v)
{
_findUrls($v);
}
}
else if(is_object($input))
{
foreach($input as $key => $value)
{
if($key === '@context')
{
// @context is an array that might contain URLs
if(is_array($value))
{
foreach($value as $idx => $v)
{
if(is_string($v))
{
// replace w/resolved @context if appropriate
if($replace)
{
$input->{$key}[$idx] = $urls[$v];
}
// unresolved @context found
else
{
$urls[$v] = new stdClass();
}
}
}
}
else if(is_string($value))
{
// replace w/resolved @context if appropriate
if($replace)
{
$input->$key = $urls[$value];
}
// unresolved @context found
else
{
$urls[$value] = new stdClass();
}
}
}
}
}
}
/**
* Gets the keywords from a context.
*
* @param ctx the context.
*
* @return the keywords.
*/
function _getKeywords($ctx)
{
// TODO: reduce calls to this function by caching keywords in processor
// state
$rval = (object)array(
'@id' => '@id',
'@language' => '@language',
'@value' => '@value',
'@type' => '@type'
);
if($ctx)
{
// gather keyword aliases from context
$keywords = new stdClass();
foreach($ctx as $key => $value)
{
if(is_string($value) and property_exists($rval, $value))
{
$keywords->{$value} = $key;
}
}
// overwrite keywords
foreach($keywords as $key => $value)
{
$rval->$key = $value;
}
}
return $rval;
}
/**
* Sets a subject's property to the given object value. If a value already
* exists, it will be appended to an array.
*
* @param s the subject.
* @param p the property.
* @param o the object.
*/
function _setProperty($s, $p, $o)
{
if(property_exists($s, $p))
{
if(is_array($s->$p))
{
array_push($s->$p, $o);
}
else
{
$s->$p = array($s->$p, $o);
}
}
else
{
$s->$p = $o;
}
}
/**
* Clones an object, array, or string/number. If cloning an object, the keys
* will be sorted.
*
* @param value the value to clone.
*
* @return the cloned value.
*/
function _clone($value)
{
$rval;
if(is_object($value))
{
$rval = new stdClass();
$keys = array_keys((array)$value);
sort($keys);
foreach($keys as $key)
{
$rval->$key = _clone($value->$key);
}
}
else if(is_array($value))
{
$rval = array();
foreach($value as $v)
{
$rval[] = _clone($v);
}
}
else
{
$rval = $value;
}
return $rval;
}
/**
* Gets the iri associated with a term.
*
* @param ctx the context.
* @param term the term.
*
* @return the iri or NULL.
*/
function _getTermIri($ctx, $term)
{
$rval = null;
if(property_exists($ctx, $term))
{
if(is_string($ctx->$term))
{
$rval = $ctx->$term;
}
else if(is_object($ctx->$term) and property_exists($ctx->$term, '@id'))
{
$rval = $ctx->$term->{'@id'};
}
}
return $rval;
}
/**
* Compacts an IRI into a term or prefix if it can be. IRIs will not be
* compacted to relative IRIs if they match the given context's default
* vocabulary.
*
* @param ctx the context to use.
* @param iri the IRI to compact.
* @param usedCtx a context to update if a value was used from "ctx".
*
* @return the compacted IRI as a term or prefix or the original IRI.
*/
function _compactIri($ctx, $iri, $usedCtx)
{
$rval = null;
// check the context for a term that could shorten the IRI
// (give preference to terms over prefixes)
foreach($ctx as $key => $value)
{
// skip special context keys (start with '@')
if(strlen($key) > 0 and $key[0] !== '@')
{
// compact to a term
if($iri === _getTermIri($ctx, $key))
{
$rval = $key;
if($usedCtx !== null)
{
$usedCtx->$key = _clone($ctx->$key);
}
break;
}
}
}
// term not found, if term is keyword, use alias
if($rval === null)
{
$keywords = _getKeywords($ctx);
if(property_exists($keywords, $iri))
{
$rval = $keywords->{$iri};
if($rval !== $iri and $usedCtx !== null)
{
$usedCtx->$rval = $iri;
}
}
}
// term not found, check the context for a prefix
if($rval === null)
{
foreach($ctx as $key => $value)
{
// skip special context keys (start with '@')
if(strlen($key) > 0 and $key[0] !== '@')
{
// see if IRI begins with the next IRI from the context
$ctxIri = _getTermIri($ctx, $key);
if($ctxIri !== null)
{
$idx = strpos($iri, $ctxIri);
// compact to a prefix
if($idx === 0 and strlen($iri) > strlen($ctxIri))
{
$rval = $key . ':' . substr($iri, strlen($ctxIri));
if($usedCtx !== null)
{
$usedCtx->$key = _clone($ctx->$key);
}
break;
}
}
}
}
}
// could not compact IRI
if($rval === null)
{
$rval = $iri;
}
return $rval;
}
/**
* Expands a term into an absolute IRI. The term may be a regular term, a
* prefix, a relative IRI, or an absolute IRI. In any case, the associated
* absolute IRI will be returned.
*
* @param ctx the context to use.
* @param term the term to expand.
* @param usedCtx a context to update if a value was used from "ctx".
*
* @return the expanded term as an absolute IRI.
*/
function _expandTerm($ctx, $term, $usedCtx)
{
$rval = $term;
// get JSON-LD keywords
$keywords = _getKeywords($ctx);
// 1. If the property has a colon, it is a prefix or an absolute IRI:
$idx = strpos($term, ':');
if($idx !== false)
{
// get the potential prefix
$prefix = substr($term, 0, $idx);
// expand term if prefix is in context, otherwise leave it be
if(property_exists($ctx, $prefix))
{
// prefix found, expand property to absolute IRI
$iri = _getTermIri($ctx, $prefix);
$rval = $iri . substr($term, $idx + 1);
if($usedCtx !== null)
{
$usedCtx->$prefix = _clone($ctx->$prefix);
}
}
}
// 2. If the property is in the context, then it's a term.
else if(property_exists($ctx, $term))
{
$rval = _getTermIri($ctx, $term);
if($usedCtx !== null)
{
$usedCtx->$term = _clone($ctx->$term);
}
}
// 3. The property is a keyword.
else
{
foreach($keywords as $key => $value)
{
if($term === $value)
{
$rval = $key;
break;
}
}
}
return $rval;
}
/**
* Gets whether or not a value is a reference to a subject (or a subject with
* no properties).
*
* @param value the value to check.
*
* @return true if the value is a reference to a subject, false if not.
*/
function _isReference($value)
{
// Note: A value is a reference to a subject if all of these hold true:
// 1. It is an Object.
// 2. It is has an @id key.
// 3. It has only 1 key.
return ($value !== null and
is_object($value) and
property_exists($value, '@id') and
count(get_object_vars($value)) === 1);
};
/**
* Gets whether or not a value is a subject with properties.
*
* @param value the value to check.
*
* @return true if the value is a subject with properties, false if not.
*/
function _isSubject($value)
{
$rval = false;
// Note: A value is a subject if all of these hold true:
// 1. It is an Object.
// 2. It is not a literal (@value).
// 3. It has more than 1 key OR any existing key is not '@id'.
if($value !== null and is_object($value) and
!property_exists($value, '@value'))
{
$keyCount = count(get_object_vars($value));
$rval = ($keyCount > 1 or !property_exists($value, '@id'));
}
return $rval;
}
function _isBlankNodeIri($v)
{
return strpos($v, '_:') === 0;
}
function _isNamedBlankNode($v)
{
// look for "_:" at the beginning of the subject
return (
is_object($v) and property_exists($v, '@id') and
_isBlankNodeIri($v->{'@id'}));
}
function _isBlankNode($v)
{
// look for a subject with no ID or a blank node ID
return (
_isSubject($v) and
(!property_exists($v, '@id') or _isNamedBlankNode($v)));
}
/**
* Compares two values.
*
* @param v1 the first value.
* @param v2 the second value.
*
* @return -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2.
*/
function _compare($v1, $v2)
{
$rval = 0;
if(is_array($v1) and is_array($v2))
{
$length = count($v1);
for($i = 0; $i < $length and $rval === 0; ++$i)
{
$rval = _compare($v1[$i], $v2[$i]);
}
}
else
{
$rval = ($v1 < $v2 ? -1 : ($v1 > $v2 ? 1 : 0));
}
return $rval;
}
/**
* Compares two keys in an object. If the key exists in one object
* and not the other, the object with the key is less. If the key exists in
* both objects, then the one with the lesser value is less.
*
* @param o1 the first object.
* @param o2 the second object.
* @param key the key.
*
* @return -1 if o1 < o2, 0 if o1 == o2, 1 if o1 > o2.
*/
function _compareObjectKeys($o1, $o2, $key)
{
$rval = 0;
if(property_exists($o1, $key))
{
if(property_exists($o2, $key))
{
$rval = _compare($o1->$key, $o2->$key);
}
else
{
$rval = -1;
}
}
else if(property_exists($o2, $key))
{
$rval = 1;
}
return $rval;
}
/**
* Compares two object values.
*
* @param o1 the first object.
* @param o2 the second object.
*
* @return -1 if o1 < o2, 0 if o1 == o2, 1 if o1 > o2.
*/
function _compareObjects($o1, $o2)
{
$rval = 0;
if(is_string($o1))
{
if(!is_string($o2))
{
$rval = -1;
}
else
{
$rval = _compare($o1, $o2);
}
}
else if(is_string($o2))
{
$rval = 1;
}
else
{
$rval = _compareObjectKeys($o1, $o2, '@value');
if($rval === 0)
{
if(property_exists($o1, '@value'))
{
$rval = _compareObjectKeys($o1, $o2, '@type');
if($rval === 0)
{
$rval = _compareObjectKeys($o1, $o2, '@language');
}
}
// both are '@id' objects
else
{
$rval = _compare($o1->{'@id'}, $o2->{'@id'});
}
}
}
return $rval;
}
/**
* Filter function for bnodes.
*
* @param e the array element.
*
* @return true to return the element in the filter results, false not to.
*/
function _filterBlankNodes($e)
{
return !_isNamedBlankNode($e);
}
/**
* Compares the object values between two bnodes.
*
* @param a the first bnode.
* @param b the second bnode.
*
* @return -1 if a < b, 0 if a == b, 1 if a > b.
*/
function _compareBlankNodeObjects($a, $b)
{
$rval = 0;
/*
3. For each property, compare sorted object values.
3.1. The bnode with fewer objects is first.
3.2. For each object value, compare only literals (@values) and non-bnodes.
3.2.1. The bnode with fewer non-bnodes is first.
3.2.2. The bnode with a string object is first.
3.2.3. The bnode with the alphabetically-first string is first.
3.2.4. The bnode with a @value is first.
3.2.5. The bnode with the alphabetically-first @value is first.
3.2.6. The bnode with the alphabetically-first @type is first.
3.2.7. The bnode with a @language is first.
3.2.8. The bnode with the alphabetically-first @language is first.
3.2.9. The bnode with the alphabetically-first @id is first.
*/
foreach($a as $p => $value)
{
// skip IDs (IRIs)
if($p !== '@id')
{
// step #3.1
$lenA = is_array($a->$p) ? count($a->$p) : 1;
$lenB = is_array($b->$p) ? count($b->$p) : 1;
$rval = _compare($lenA, $lenB);
// step #3.2.1
if($rval === 0)
{
// normalize objects to an array
$objsA = $a->$p;
$objsB = $b->$p;
if(!is_array($objsA))
{
$objsA = array($objsA);
$objsB = array($objsB);
}
// compare non-bnodes (remove bnodes from comparison)
$objsA = array_filter($objsA, '_filterBlankNodes');
$objsB = array_filter($objsB, '_filterBlankNodes');
$objsALen = count($objsA);
$rval = _compare($objsALen, count($objsB));
}
// steps #3.2.2-3.2.9
if($rval === 0)
{
usort($objsA, '_compareObjects');
usort($objsB, '_compareObjects');
for($i = 0; $i < $objsALen and $rval === 0; ++$i)
{
$rval = _compareObjects($objsA[$i], $objsB[$i]);
}
}
if($rval !== 0)
{
break;
}
}
}
return $rval;
}
/**
* A blank node name generator that uses a prefix and counter.
*/
class NameGenerator
{
public function __construct($prefix)
{
$this->count = -1;
$this->base = '_:' . $prefix;
}
public function next()
{
$this->count += 1;
return $this->current();
}
public function current()
{
return $this->base . $this->count;
}
public function inNamespace($iri)
{
return strpos($iri, $this->base) === 0;
}
}
/**
* Populates a map of all named subjects from the given input and an array
* of all unnamed bnodes (includes embedded ones).
*
* @param input the input (must be expanded, no context).
* @param subjects the subjects map to populate.
* @param bnodes the bnodes array to populate.
*/
function _collectSubjects($input, $subjects, $bnodes)
{
if($input === null)
{
// nothing to collect
}
else if(is_array($input))
{
foreach($input as $value)
{
_collectSubjects($value, $subjects, $bnodes);
}
}
else if(is_object($input))
{
if(property_exists($input, '@id'))
{
// graph literal/disjoint graph
if(is_array($input->{'@id'}))
{
_collectSubjects($input->{'@id'}, $subjects, $bnodes);
}
// named subject
else if(_isSubject($input))
{
$subjects->{$input->{'@id'}} = $input;
}
}
// unnamed blank node
else if(_isBlankNode($input))
{
$bnodes[] = $input;
}
// recurse through subject properties
foreach($input as $value)
{
_collectSubjects($value, $subjects, $bnodes);
}
}
}
/**
* Filters duplicate objects.
*/
class DuplicateFilter
{
public function __construct($obj)
{
$this->obj = $obj;
}
public function filter($e)
{
return (_compareObjects($e, $this->obj) === 0);
}
}
/**
* Flattens the given value into a map of unique subjects. It is assumed that
* all blank nodes have been uniquely named before this call. Array values for
* properties will be sorted.
*
* @param parent the value's parent, NULL for none.
* @param parentProperty the property relating the value to the parent.
* @param value the value to flatten.
* @param subjects the map of subjects to write to.
*/
function _flatten($parent, $parentProperty, $value, $subjects)
{
$flattened = null;
if($value === null)
{
// drop null values
}
else if(is_array($value))
{
// list of objects or a disjoint graph
foreach($value as $v)
{
_flatten($parent, $parentProperty, $v, $subjects);
}
}
else if(is_object($value))
{
// already-expanded value or special-case reference-only @type
if(property_exists($value, '@value') or $parentProperty === '@type')
{
$flattened = _clone($value);
}
// graph literal/disjoint graph
else if(is_array($value->{'@id'}))
{
// cannot flatten embedded graph literals
if($parent !== null)
{
throw new Exception('Embedded graph literals cannot be flattened.');
}
// top-level graph literal
foreach($value->{'@id'} as $k => $v)
{
_flatten($parent, $parentProperty, $v, $subjects);
}
}
// regular subject
else
{
// create or fetch existing subject
if(property_exists($subjects, $value->{'@id'}))
{
// FIXME: '@id' might be a graph literal (as {})
$subject = $subjects->{$value->{'@id'}};
}
else
{
// FIXME: '@id' might be a graph literal (as {})
$subject = new stdClass();
$subject->{'@id'} = $value->{'@id'};
$subjects->{$value->{'@id'}} = $subject;
}
$flattened = new stdClass();
$flattened->{'@id'} = $subject->{'@id'};
// flatten embeds
foreach($value as $key => $v)
{
// drop null values, skip @id (it is already set above)
if($v !== null and $key !== '@id')
{
if(property_exists($subject, $key))
{
if(!is_array($subject->$key))
{
$subject->$key = new ArrayObject(array($subject->$key));
}
else
{
$subject->$key = new ArrayObject($subject->$key);
}
}
else
{
$subject->$key = new ArrayObject();
}
_flatten($subject->$key, $key, $value->$key, $subjects);
$subject->$key = (array)$subject->$key;
if(count($subject->$key) === 1)
{
// convert subject[key] to object if it has only 1
$arr = $subject->$key;
$subject->$key = $arr[0];
}
}
}
}
}
// string value
else
{
$flattened = $value;
}
// add flattened value to parent
if($flattened !== null and $parent !== null)
{
if($parent instanceof ArrayObject)
{
// do not add duplicate IRIs for the same property
$duplicate = count(array_filter(
(array)$parent, array(
new DuplicateFilter($flattened), 'filter'))) > 0;
if(!$duplicate)
{
$parent[] = $flattened;
}
}
else
{
$parent->$parentProperty = $flattened;
}
}
}
/**
* A MappingBuilder is used to build a mapping of existing blank node names
* to a form for serialization. The serialization is used to compare blank
* nodes against one another to determine a sort order.
*/
class MappingBuilder
{
/**
* Constructs a new MappingBuilder.
*/
public function __construct()
{
$this->count = 1;
$this->processed = new stdClass();
$this->mapping = new stdClass();
$this->adj = new stdClass();
$this->keyStack = array();
$entry = new stdClass();
$entry->keys = array('s1');
$entry->idx = 0;
$this->keyStack[] = $entry;
$this->done = new stdClass();
$this->s = '';
}
/**
* Copies this MappingBuilder.
*
* @return the MappingBuilder copy.
*/
public function copy()
{
$rval = new MappingBuilder();
$rval->count = $this->count;
$rval->processed = _clone($this->processed);
$rval->mapping = _clone($this->mapping);
$rval->adj = _clone($this->adj);
$rval->keyStack = _clone($this->keyStack);
$rval->done = _clone($this->done);
$rval->s = $this->s;
return $rval;
}
/**
* Maps the next name to the given bnode IRI if the bnode IRI isn't already
* in the mapping. If the given bnode IRI is canonical, then it will be
* given a shortened form of the same name.
*
* @param iri the blank node IRI to map the next name to.
*
* @return the mapped name.
*/
public function mapNode($iri)
{
if(!property_exists($this->mapping, $iri))
{
if(strpos($iri, '_:c14n') === 0)
{
$this->mapping->$iri = 'c' . substr($iri, 6);
}
else
{
$this->mapping->$iri = 's' . $this->count++;
}
}
return $this->mapping->$iri;
}
}
/**
* Rotates the elements in an array one position.
*
* @param a the array.
*/
function _rotate(&$a)
{
$e = array_shift($a);
array_push($a, $e);
return $e;
}
/**
* Compares two serializations for the same blank node. If the two
* serializations aren't complete enough to determine if they are equal (or if
* they are actually equal), 0 is returned.
*
* @param s1 the first serialization.
* @param s2 the second serialization.
*
* @return -1 if s1 < s2, 0 if s1 == s2 (or indeterminate), 1 if s1 > v2.
*/
function _compareSerializations($s1, $s2)
{
$rval = 0;
$s1Len = strlen($s1);
$s2Len = strlen($s2);
if($s1Len == $s2Len)
{
$rval = strcmp($s1, $s2);
}
else
{
$rval = strncmp($s1, $s2, ($s1Len > $s2Len) ? $s2Len : $s1Len);
}
return $rval;
}
/**
* Compares two nodes based on their iris.
*
* @param a the first node.
* @param b the second node.
*
* @return -1 if iriA < iriB, 0 if iriA == iriB, 1 if iriA > iriB.
*/
function _compareIris($a, $b)
{
return _compare($a->{'@id'}, $b->{'@id'});
}
/**
* Filters blank node edges.
*
* @param e the edge to check.
*
* @return true if the edge is a blank node edge.
*/
function _filterBlankNodeEdges($e)
{
return _isBlankNodeIri($e->s);
}
/**
* Sorts mapping keys based on their associated mapping values.
*/
class MappingKeySorter
{
public function __construct($mapping)
{
$this->mapping = $mapping;
}
public function compare($a, $b)
{
return _compare($this->mapping->$a, $this->mapping->$b);
}
function jsonld_process_context($active_ctx, $local_ctx, $options=array()) {
$p = new JsonLdProcessor();
return $p->processContext($active_ctx, $local_ctx, $options);
}
/**
* A JSON-LD processor.
*/
class JsonLdProcessor
{
/**
* Constructs a JSON-LD processor.
*/
public function __construct()
{
}
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';
/**
* Recursively compacts a value. This method will compact IRIs to prefixes
* or terms and do reverse type coercion to compact a value.
*
* @param ctx the context to use.
* @param property the property that points to the value, NULL for none.
* @param value the value to compact.
* @param usedCtx a context to update if a value was used from "ctx".
*
* @return the compacted value.
*/
public function compact($ctx, $property, $value, $usedCtx)
{
$rval;
/** 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';
// get JSON-LD keywords
$keywords = _getKeywords($ctx);
/**
* Constructs a JSON-LD processor.
*/
public function __construct() {}
if($value === null)
{
// return null, but check coerce type to add to usedCtx
$rval = null;
$this->getCoerceType($ctx, $property, $usedCtx);
/**
* 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.
* [activeCtx] true to also return the active context used.
*
* @return mixed the compacted JSON-LD output.
*/
public function compact($input, $ctx, $options) {
// nothing to compact
if($input === null) {
return null;
}
// set default options
isset($options['base']) or $options['base'] = '';
isset($options['strict']) or $options['strict'] = true;
isset($options['optimize']) or $options['optimize'] = false;
isset($options['graph']) or $options['graph'] = false;
isset($options['activeCtx']) or $options['activeCtx'] = false;
// FIXME: implement jsonld_resolve_url
isset($options['resolver']) or $options['resolver'] = 'jsonld_resolve_url';
// 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();
try {
$active_ctx = $this->processContext($active_ctx, $ctx, $options);
}
catch(JsonLdException $e) {
throw new JsonLdException(
'Could not merge context before compaction.',
'jsonld.CompactError', null, $e);
}
// do compaction
$compacted = $this->_compact($active_ctx, null, $input, $options);
// always use an array if graph options is on
if($options['graph'] === true) {
$output = self::arrayify();
}
// else if compacted is an array with 1 entry, remove array
else if(is_array($output) && count($output) === 1) {
$output = $output[0];
}
// build output context
$ctx = self::copy($ctx);
$ctx = self::arrayify($ctx);
// remove empty contexts
$tmp = $ctx;
$ctx = array();
foreach($tmp as $i => $v) {
if(!is_object($v) || count(get_object_vars($v)) > 0) {
$ctx[] = $v;
}
else if(is_array($value))
{
// recursively add compacted values to array
$rval = array();
foreach($value as $v)
{
$rval[] = $this->compact($ctx, $property, $v, $usedCtx);
}
}
// remove array if only one context
$ctx_length = count($ctx);
$has_context = ($ctx_length > 0);
if($ctx_length === 1) {
$ctx = $ctx[0];
}
// add context
if($has_context || $options['graph']) {
if(is_array($compacted)) {
// use '@graph' keyword
$kwgraph =$this->_compactIri($active_ctx, '@graph');
$graph = $compacted;
$compacted = new stdClass();
if($hasContext) {
$compacted->{'@context'} = $ctx;
}
$compacted->{$kwgraph} = $graph;
}
// graph literal/disjoint graph
else if(
is_object($value) and
property_exists($value, '@id') and
is_array($value->{'@id'}))
{
$rval = new stdClass();
$rval->{$keywords->{'@id'}} = $this->compact(
$ctx, $property, $value->{'@id'}, $usedCtx);
else if(is_object($compacted)) {
// reorder keys so @context is first
$graph = $compacted;
$compacted = new stdClass();
$compacted->{'@context'} = $ctx;
foreach($graph as $k => $v) {
$compacted->{$k} = $v;
}
}
// recurse if value is a subject
else if(_isSubject($value))
{
// recursively handle sub-properties that aren't a sub-context
$rval = new stdClass();
foreach($value as $key => $v)
{
if($v !== '@context')
{
// set object to compacted property, only overwrite existing
// properties if the property actually compacted
$p = _compactIri($ctx, $key, $usedCtx);
if($p !== $key or !property_exists($rval, $p))
{
$rval->$p = $this->compact($ctx, $key, $v, $usedCtx);
}
}
}
}
if($options['activeCtx']) {
return array(
'compacted' => $compacted,
'activeCtx' => $active_ctx);
}
else {
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.
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return array the expanded JSON-LD output.
*/
public function expand($input, $options) {
// set default options
isset($options['base']) or $options['base'] = '';
// FIXME: implement jsonld_resolve_url
isset($options['resolver']) or $options['resolver'] = 'jsonld_resolve_url';
// resolve all @context URLs in the input
$input = self::copy($input);
$this->_resolveUrls($input, $options['resolver']);
// do expansion
$ctx = $this->_getInitialContext();
$expanded = $this->_expand($ctx, null, $input, $options, false);
// optimize away @graph with no other properties
if(is_object($expanded) && property_exists($expanded, '@graph') &&
count(get_object_vars($expanded)) === 1) {
$expanded = $expanded->{'@graph'};
}
// normalize to an array
return self::arrayify($expanded);
}
/**
* 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.
* [embed] default @embed flag (default: true).
* [explicit] default @explicit flag (default: false).
* [omitDefault] default @omitDefault flag (default: false).
* [optimize] optimize when compacting (default: false).
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return stdClass the framed JSON-LD output.
*/
public function frame($input, $frame, $options) {
// set default options
isset($options['base']) or $options['base'] = '';
isset($options['embed']) or $options['embed'] = true;
isset($options['explicit']) or $options['explicit'] = false;
isset($options['omitDefault']) or $options['omitDefault'] = false;
isset($options['optimize']) or $options['optimize'] = false;
// FIXME: implement jsonld_resolve_url
isset($options['resolver']) or $options['resolver'] = 'jsonld_resolve_url';
// preserve frame context
$ctx = $frame->{'@context'} ?: new stdClass();
try {
// expand input
$_input = $this->expand($input, $options);
}
catch(JsonLdException $e) {
throw new JsonLdException(
'Could not expand input before framing.',
'jsonld.FrameError', $e);
}
try {
// expand frame
$_frame = $this->expand($frame, $options);
}
catch(JsonLdException $e) {
throw new JsonLdException(
'Could not expand frame before framing.',
'jsonld.FrameError', $e);
}
// do framing
$framed = $this->_frame($_input, $_frame, $options);
try {
// compact result (force @graph option to true)
$options['graph'] = true;
$options['activeCtx'] = true;
$result = $this->compact($framed, $ctx, $options);
}
catch(JsonLdException $e) {
throw new JsonLdException(
'Could not compact framed output.',
'jsonld.FrameError', $e);
}
$compacted = $result['compacted'];
$ctx = $result['activeCtx'];
// get graph alias
$graph = $this->_compactIri($ctx, '@graph');
// remove @preserve from results
$compacted->{$graph} = $this->_removePreserve($ctx, $compacted->{$graph});
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.
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return array the JSON-LD normalized output.
*/
public function normalize($input, $options) {
// set default options
isset($options['base']) or $options['base'] = '';
// FIXME: implement jsonld_resolve_url
isset($options['resolver']) or $options['resolver'] = 'jsonld_resolve_url';
try {
// expand input then do normalization
$expanded = $this->expand($input, $options);
}
catch(JsonLdException $e) {
throw new JsonLdException(
'Could not expand input before normalization.',
'jsonld.NormalizeError', $e);
}
// do normalization
$this->_normalize($expanded);
}
/**
* Outputs the RDF statements found in the given JSON-LD object.
*
* @param mixed $input the JSON-LD object.
* @param assoc $options the options to use:
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return array the RDF statements.
*/
public function toRDF($input, $options) {
// set default options
isset($options['base']) or $options['base'] = '';
// FIXME: implement jsonld_resolve_url
isset($options['resolver']) or $options['resolver'] = 'jsonld_resolve_url';
// resolve all @context URLs in the input
$input = self::copy($input);
$this->_resolveUrls($input, $options['resolver']);
// output RDF statements
return $this->_toRDF($input);
}
/**
* 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:
* [resolver(url, callback(err, jsonCtx))] the URL resolver to use.
*
* @return stdClass the new active context.
*/
public function processContext($active_ctx, $local_ctx) {
// return initial context early for null context
if($local_ctx === null) {
return $this->_getInitialContext();
}
// set default options
isset($options['base']) or $options['base'] = '';
// FIXME: implement jsonld_resolve_url
isset($options['resolver']) or $options['resolver'] = 'jsonld_resolve_url';
// resolve URLs in localCtx
$local_ctx = self::copy($local_ctx);
if(is_object($local_ctx) && !property_exists($local_ctx, '@context')) {
$local_ctx = (object)array('@context' => $local_ctx);
}
$ctx = $this->_resolveUrls($local_ctx, $options['resolver']);
// process context
return $this->_processContext($active_ctx, $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};
$isList = self::_isListValue($val);
if(is_array($val) || $isList) {
if($isList) {
$val = $val->{'@list'};
}
foreach($val as $v) {
if(self::compareValues($value, $v)) {
$rval = true;
break;
}
}
}
else
{
// get coerce type
$coerce = $this->getCoerceType($ctx, $property, $usedCtx);
// avoid matching the set of values with an array value parameter
else if(!is_array($value)) {
$rval = self::compareValues($value, $val);
}
}
return $rval;
}
// get type from value, to ensure coercion is valid
$type = null;
if(is_object($value))
{
// type coercion can only occur if language is not specified
if(!property_exists($value, '@language'))
{
// type must match coerce type if specified
if(property_exists($value, '@type'))
{
$type = $value->{'@type'};
}
// type is ID (IRI)
else if(property_exists($value, '@id'))
{
$type = '@id';
}
// can be coerced to any type
else
{
$type = $coerce;
}
}
}
// type can be coerced to anything
else if(is_string($value))
{
$type = $coerce;
}
/**
* Adds a value to a subject. If the subject already has the value, it will
* not be added. 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 bool [$propertyIsArray] true if the property is always an array,
* false if not (default: false).
*/
public static function addValue(
$subject, $property, $value, $propertyIsArray=false) {
if(is_array($value)) {
if(count($value) === 0 && $propertyIsArray &&
!property_exists($subject, $property)) {
$subject->{$property} = array();
}
foreach($value as $v) {
self::addValue($subject, $property, $v, $propertyIsArray);
}
}
else if(property_exists($subject, $property)) {
$hasValue = self::hasValue($subject, $property, $value);
// types that can be auto-coerced from a JSON-builtin
if($coerce === null and
($type === JSONLD_XSD_BOOLEAN or $type === JSONLD_XSD_INTEGER or
$type === JSONLD_XSD_DOUBLE))
{
$coerce = $type;
}
// do reverse type-coercion
if($coerce !== null)
{
// type is only null if a language was specified, which is an error
// if type coercion is specified
if($type === null)
{
throw new Exception(
'Cannot coerce type when a language is specified. ' .
'The language information would be lost.');
}
// if the value type does not match the coerce type, it is an error
else if($type !== $coerce)
{
throw new Exception(
'Cannot coerce type because the type does not match.');
}
// do reverse type-coercion
else
{
if(is_object($value))
{
if(property_exists($value, '@id'))
{
$rval = $value->{'@id'};
}
else if(property_exists($value, '@value'))
{
$rval = $value->{'@value'};
}
}
else
{
$rval = $value;
}
// do basic JSON types conversion
if($coerce === JSONLD_XSD_BOOLEAN)
{
$rval = ($rval === 'true' or $rval != 0);
}
else if($coerce === JSONLD_XSD_DOUBLE)
{
$rval = floatval($rval);
}
else if($coerce === JSONLD_XSD_INTEGER)
{
$rval = intval($rval);
}
}
}
// no type-coercion, just change keywords/copy value
else if(is_object($value))
{
$rval = new stdClass();
foreach($value as $key => $v)
{
$rval->{$keywords->$key} = $v;
}
}
else
{
$rval = _clone($value);
}
// compact IRI
if($type === '@id')
{
if(is_object($rval))
{
$rval->{$keywords->{'@id'}} = _compactIri(
$ctx, $rval->{$keywords->{'@id'}}, $usedCtx);
}
else
{
$rval = _compactIri($ctx, $rval, $usedCtx);
}
}
// make property an array if value not present or always an array
if(!is_array($subject->{$property}) && (!$hasValue || $propertyIsArray)) {
$subject->{$property} = array($subject->{$property});
}
// add new value
if(!$hasValue) {
$subject->{$property}[] = $value;
}
}
else {
// add new value as set or single value
$subject->{$property} = $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 = $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 bool [$propertyIsArray] true if the property is always an array,
* false if not (default: false).
*/
public static function removeValue(
$subject, $property, $value, $propertyIsArray=false) {
// filter out value
$filter = function($e) use ($value) {
return !self::compareValues($e, $value);
};
$values = self::getValues($subject, $property);
$values = array_filter($values, $filter);
if(count($values) === 0) {
self::removeProperty($subject, $property);
}
else if(count($values) === 1 && !$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, and @language, OR
* 3. They both have @ids they 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) &&
$v1->{'@value'} === $v2->{'@value'} &&
property_exists($v1, '@type') === property_exists($v2, '@type') &&
property_exists($v1, '@language') === property_exists($v2, '@language') &&
(!property_exists($v1, '@type') || $v1->{'@type'} === $v2->{'@type'}) &&
(!property_exists($v1, '@language') ||
$v2->{'@language'} === $v2->{'@language'})) {
return true;
}
// 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;
}
/**
* Compares two JSON-LD normalized inputs for equality.
*
* @param array $n1 the first normalized input.
* @param array $n2 the second normalized input.
*
* @return bool true if the inputs are equivalent, false if not.
*/
public static function compareNormalized($n1, $n2) {
if(!is_array($n1) || !is_array($n2)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; normalized JSON-LD must be an array.',
'jsonld.SyntaxError');
}
// different # of subjects
if(count($n1) !== count($n2)) {
return false;
}
// assume subjects are in the same order because of normalization
foreach($n1 as $i => $s1) {
$s2 = $n2[$i];
// different @ids
if($s1->{'@id'} !== $s2->{'@id'}) {
return false;
}
// subjects have different properties
if(count(get_object_vars($s1)) !== count(get_object_vars($s2))) {
return false;
}
foreach($s1 as $p => $objects) {
// skip @id property
if($p === '@id') {
continue;
}
// s2 is missing s1 property
if(!self::hasProperty($s2, $p)) {
return false;
}
// subjects have different objects for the property
if(count($objects) !== count($s2->{$p})) {
return false;
}
foreach($objects as $oi => $o) {
// s2 is missing s1 object
if(!self::hasValue($s2, $p, $o)) {
return false;
}
}
}
}
return true;
}
/**
* 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;
}
}
/**
* Recursively expands a value using the given context. Any context in
* the value will be removed.
*
* @param ctx the context.
* @param property the property that points to the value, NULL for none.
* @param value the value to expand.
*
* @return the expanded value.
*/
public function expand($ctx, $property, $value)
{
$rval;
// get default language
if($type === '@language' && property_exists($ctx, $type)) {
$rval = $ctx->{$type};
}
// TODO: add data format error detection?
// get specific entry information
if(property_exists($ctx->mappings, $key)) {
$entry = $ctx->mappings->{$key};
// value is null, nothing to expand
if($value === null)
{
$rval = null;
// return whole entry
if($type === null) {
$rval = $entry;
}
// if no property is specified and the value is a string (this means the
// value is a property itself), expand to an IRI
else if($property === null and is_string($value))
{
$rval = _expandTerm($ctx, $value, null);
// return entry value for type
else if(property_exists($entry, $type)) {
$rval = $entry->{$type};
}
else if(is_array($value))
{
// recursively add expanded values to array
$rval = array();
foreach($value as $v)
{
$rval[] = $this->expand($ctx, $property, $v);
}
}
else if(is_object($value))
{
// if value has a context, use it
if(property_exists($value, '@context'))
{
$ctx = jsonld_merge_contexts($ctx, $value->{'@context'});
}
}
// recursively handle sub-properties that aren't a sub-context
$rval = new stdClass();
foreach($value as $key => $v)
{
// preserve frame keywords
if($key === '@embed' or $key === '@explicit' or
$key === '@default' or $key === '@omitDefault')
{
_setProperty($rval, $key, _clone($v));
}
else if($key !== '@context')
{
// set object to expanded property
_setProperty(
$rval, _expandTerm($ctx, $key, null),
$this->expand($ctx, $key, $v));
}
}
}
else
{
// do type coercion
$coerce = $this->getCoerceType($ctx, $property, null);
return $rval;
}
// get JSON-LD keywords
$keywords = _getKeywords($ctx);
/**
* If $value is an array, returns $value, otherwise returns an array
* containing $value as the only element.
*
* @param mixed $value the value.
*
* @return an array.
*/
public static function arrayify($value) {
if(is_array($value)) {
return $value;
}
return array($value);
}
// automatic coercion for basic JSON types
if($coerce === null and !is_string($value) and
(is_numeric($value) or is_bool($value)))
{
if(is_bool($value))
{
$coerce = JSONLD_XSD_BOOLEAN;
}
else if(is_int($value))
{
$coerce = JSONLD_XSD_INTEGER;
}
else
{
$coerce = JSONLD_XSD_DOUBLE;
}
}
/**
* 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;
}
}
// special-case expand @id and @type (skips '@id' expansion)
if($property === '@id' or $property === $keywords->{'@id'} or
$property === '@type' or $property === $keywords->{'@type'})
{
$rval = _expandTerm($ctx, $value, null);
}
// coerce to appropriate type
else if($coerce !== null)
{
$rval = new stdClass();
// expand ID (IRI)
if($coerce === '@id')
{
$rval->{'@id'} = _expandTerm($ctx, $value, null);
}
// other type
else
{
$rval->{'@type'} = $coerce;
if($coerce === JSONLD_XSD_DOUBLE)
{
// do special JSON-LD double format
$value = preg_replace(
'/(e(?:\+|-))([0-9])$/', '${1}0${2}',
sprintf('%1.6e', $value));
}
else if($coerce === JSONLD_XSD_BOOLEAN)
{
$value = $value ? 'true' : 'false';
}
$rval->{'@value'} = '' . $value;
}
}
// nothing to coerce
else
{
$rval = '' . $value;
}
}
return $rval;
}
/**
* Normalizes a JSON-LD object.
*
* @param input the JSON-LD object to normalize.
*
* @return the normalized JSON-LD object.
*/
public function normalize($input)
{
/**
* Recursively compacts an element using the given active context. All values
* must be in expanded form before this method is called.
*
* @param stdClass $ctx the active context to use.
* @param mixed $property the property that points to the element, null for
* none.
* @param mixed $element the element to compact.
* @param array $options the compaction options.
*
* @return mixed the compacted value.
*/
protected function _compact($ctx, $property, $element, $options) {
// recursively compact array
if(is_array($element)) {
$rval = array();
// TODO: validate context
if($input !== null)
{
// create name generator state
$this->ng = new stdClass();
$this->ng->tmp = null;
$this->ng->c14n = null;
// expand input
$expanded = $this->expand(new stdClass(), null, $input);
// assign names to unnamed bnodes
$this->nameBlankNodes($expanded);
// flatten
$subjects = new stdClass();
_flatten(null, null, $expanded, $subjects);
// append subjects with sorted properties to array
foreach($subjects as $key => $s)
{
$sorted = new stdClass();
$keys = array_keys((array)$s);
sort($keys);
foreach($keys as $i => $k)
{
$sorted->$k = $s->$k;
}
$rval[] = $sorted;
}
// canonicalize blank nodes
$this->canonicalizeBlankNodes($rval);
// sort output
usort($rval, '_compareIris');
foreach($element as $e) {
$e = $this->_compact($ctx, $property, $e, $options);
// drop null values
if($e !== null) {
$rval[] = $e;
}
}
return $rval;
}
/**
* Gets the coerce type for the given property.
*
* @param ctx the context to use.
* @param property the property to get the coerced type for.
* @param usedCtx a context to update if a value was used from "ctx".
*
* @return the coerce type, null for none.
*/
public function getCoerceType($ctx, $property, $usedCtx)
{
$rval = null;
// get expanded property
$p = _expandTerm($ctx, $property, null);
// built-in type coercion JSON-LD-isms
if($p === '@id' or $p === '@type')
{
$rval = '@id';
}
else
{
// look up compacted property in coercion map
$p = _compactIri($ctx, $p, null);
if(property_exists($ctx, $p) and is_object($ctx->$p) and
property_exists($ctx->$p, '@type'))
{
// property found, return expanded type
$type = $ctx->$p->{'@type'};
$rval = _expandTerm($ctx, $type, $usedCtx);
if($usedCtx !== null)
{
$usedCtx->$p = _clone($ctx->$p);
}
}
}
return $rval;
}
/**
* Assigns unique names to blank nodes that are unnamed in the given input.
*
* @param input the input to assign names to.
*/
public function nameBlankNodes($input)
{
// create temporary blank node name generator
$ng = $this->ng->tmp = new NameGenerator('tmp');
// collect subjects and unnamed bnodes
$subjects = new stdClass();
$bnodes = new ArrayObject();
_collectSubjects($input, $subjects, $bnodes);
// uniquely name all unnamed bnodes
foreach($bnodes as $i => $bnode)
{
if(!property_exists($bnode, '@id'))
{
// generate names until one is unique
while(property_exists($subjects, $ng->next()));
$bnode->{'@id'} = $ng->current();
$subjects->{$ng->current()} = $bnode;
}
}
}
/**
* Renames a blank node, changing its references, etc. The method assumes
* that the given name is unique.
*
* @param b the blank node to rename.
* @param id the new name to use.
*/
public function renameBlankNode($b, $id)
{
$old = $b->{'@id'};
// update bnode IRI
$b->{'@id'} = $id;
// update subjects map
$subjects = $this->subjects;
$subjects->$id = $subjects->$old;
unset($subjects->$old);
// update reference and property lists
$this->edges->refs->$id = $this->edges->refs->$old;
$this->edges->props->$id = $this->edges->props->$old;
unset($this->edges->refs->$old);
unset($this->edges->props->$old);
// update references to this bnode
$refs = $this->edges->refs->$id->all;
foreach($refs as $i => $r)
{
$iri = $r->s;
if($iri === $old)
{
$iri = $id;
}
$ref = $subjects->$iri;
$props = $this->edges->props->$iri->all;
foreach($props as $prop)
{
if($prop->s === $old)
{
$prop->s = $id;
// normalize property to array for single code-path
$p = $prop->p;
$tmp = is_object($ref->$p) ? array($ref->$p) :
(is_array($ref->$p) ? $ref->$p : array());
$length = count($tmp);
for($n = 0; $n < $length; ++$n)
{
if(is_object($tmp[$n]) and
property_exists($tmp[$n], '@id') and
$tmp[$n]->{'@id'} === $old)
{
$tmp[$n]->{'@id'} = $id;
}
}
}
}
}
// update references from this bnode
$props = $this->edges->props->$id->all;
foreach($props as $prop)
{
$iri = $prop->s;
$refs = $this->edges->refs->$iri->all;
foreach($refs as $r)
{
if($r->s === $old)
{
$r->s = $id;
}
}
}
}
/**
* Canonically names blank nodes in the given input.
*
* @param input the flat input graph to assign names to.
*/
public function canonicalizeBlankNodes($input)
{
// create serialization state
$this->renamed = new stdClass();
$this->mappings = new stdClass();
$this->serializations = new stdClass();
// collect subjects and bnodes from flat input graph
$edges = $this->edges = new stdClass();
$edges->refs = new stdClass();
$edges->props = new stdClass();
$subjects = $this->subjects = new stdClass();
$bnodes = array();
foreach($input as $v)
{
$iri = $v->{'@id'};
$subjects->$iri = $v;
$edges->refs->$iri = new stdClass();
$edges->refs->$iri->all = array();
$edges->refs->$iri->bnodes = array();
$edges->props->$iri = new stdClass();
$edges->props->$iri->all = array();
$edges->props->$iri->bnodes = array();
if(_isBlankNodeIri($iri))
{
$bnodes[] = $v;
}
}
// collect edges in the graph
$this->collectEdges();
// create canonical blank node name generator
$c14n = $this->ng->c14n = new NameGenerator('c14n');
$ngTmp = $this->ng->tmp;
// rename all bnodes that happen to be in the c14n namespace
// and initialize serializations
foreach($bnodes as $i => $bnode)
{
$iri = $bnode->{'@id'};
if($c14n->inNamespace($iri))
{
// generate names until one is unique
while(property_exists($subjects, $ngTmp->next()));
$this->renameBlankNode($bnode, $ngTmp->current());
$iri = $bnode->{'@id'};
}
$this->serializations->$iri = new stdClass();
$this->serializations->$iri->props = null;
$this->serializations->$iri->refs = null;
}
// keep sorting and naming blank nodes until they are all named
$resort = true;
while(count($bnodes) > 0)
{
if($resort)
{
$resort = false;
usort($bnodes, array($this, 'deepCompareBlankNodes'));
}
// name all bnodes according to the first bnode's relation mappings
// (if it has mappings then a resort will be necessary)
$bnode = array_shift($bnodes);
$iri = $bnode->{'@id'};
$resort = ($this->serializations->$iri->{'props'} !== null);
$dirs = array('props', 'refs');
foreach($dirs as $dir)
{
// if no serialization has been computed, name only the first node
if($this->serializations->$iri->$dir === null)
{
$mapping = new stdClass();
$mapping->$iri = 's1';
}
else
{
$mapping = $this->serializations->$iri->$dir->m;
}
// sort keys by value to name them in order
$keys = array_keys((array)$mapping);
usort($keys, array(new MappingKeySorter($mapping), 'compare'));
// name bnodes in mapping
$renamed = array();
foreach($keys as $i => $iriK)
{
if(!$c14n->inNamespace($iri) and
property_exists($subjects, $iriK))
{
$this->renameBlankNode($subjects->$iriK, $c14n->next());
$renamed[] = $iriK;
}
}
// only keep non-canonically named bnodes
$tmp = $bnodes;
$bnodes = array();
foreach($tmp as $i => $b)
{
$iriB = $b->{'@id'};
if(!$c14n->inNamespace($iriB))
{
// mark serializations related to the named bnodes as dirty
foreach($renamed as $r)
{
if($this->markSerializationDirty($iriB, $r, $dir))
{
// resort if a serialization was marked dirty
$resort = true;
}
}
$bnodes[] = $b;
}
}
}
}
// sort property lists that now have canonically-named bnodes
foreach($edges->props as $key => $value)
{
$subject = $subjects->$key;
foreach($subject as $p => $v)
{
if(strpos($p, '@id') !== 0 and is_array($v))
{
usort($v, '_compareObjects');
$subject->$p = $v;
}
}
}
}
/**
* Marks a relation serialization as dirty if necessary.
*
* @param iri the IRI of the bnode to check.
* @param changed the old IRI of the bnode that changed.
* @param dir the direction to check ('props' or 'refs').
*
* @return true if the serialization was marked dirty, false if not.
*/
public function markSerializationDirty($iri, $changed, $dir)
{
$rval = false;
$s = $this->serializations->$iri;
if($s->$dir !== null and property_exists($s->$dir->m, $changed))
{
$s->$dir = null;
$rval = true;
if(count($rval) === 1) {
// use single element if no container is specified
$container = self::getContextValue($ctx, $property, '@container');
if($container !== '@list' && $container !== '@set') {
$rval = $rval[0];
}
}
return $rval;
}
}
/**
* Serializes the properties of the given bnode for its relation
* serialization.
*
* @param b the blank node.
*
* @return the serialized properties.
*/
public function serializeProperties($b)
{
$rval = '';
// recursively compact object
if(is_object($element)) {
// element is a @value
if(self::_isValue($element)) {
$type = self::getContextValue($ctx, $property, '@type');
$language = self::getContextValue($ctx, $property, '@language');
$first = true;
foreach($b as $p => $o)
{
if($p !== '@id')
{
if($first)
{
$first = false;
}
else
{
$rval .= '|';
}
// matching @type specified in context, compact element
if($type !== null &&
property_exists($element, '@type') && $element->{'@type'} === $type) {
$element = $element->{'@value'};
// property
$rval .= '<' . $p . '>';
// object(s)
$objs = is_array($o) ? $o : array($o);
foreach($objs as $obj)
{
if(is_object($obj))
{
// ID (IRI)
if(property_exists($obj, '@id'))
{
if(_isBlankNodeIri($obj->{'@id'}))
{
$rval .= '_:';
}
else
{
$rval .= '<' . $obj->{'@id'} . '>';
}
}
// literal
else
{
$rval .= '"' . $obj->{'@value'} . '"';
// type literal
if(property_exists($obj, '@type'))
{
$rval .= '^^<' . $obj->{'@type'} . '>';
}
// language literal
else if(property_exists($obj, '@language'))
{
$rval .= '@' . $obj->{'@language'};
}
}
}
// plain literal
else
{
$rval .= '"' . $obj . '"';
}
}
}
// use native datatypes for certain xsd types
if($type === self::XSD_BOOLEAN) {
$element = !($element === 'false' || $element === '0');
}
else if($type === self::XSD_INTEGER) {
$element = intval($element);
}
else if($type === self::XSD_DOUBLE) {
$element = doubleval($element);
}
}
// matching @language specified in context, compact element
else if($language !== null &&
property_exists($element, '@language') &&
$element->{'@language'} === $language) {
$element = $element->{'@value'};
}
// compact @type IRI
else if(property_exists($element, '@type')) {
$element->{'@type'} = $this->_compactIri($ctx, $element->{'@type'});
}
return $element;
}
// compact subject references
if(self::_isSubjectReference($element)) {
$type = self::getContextValue($ctx, $property, '@type');
if($type === '@id') {
$element = $this->_compactIri($ctx, $element->{'@id'});
return $element;
}
}
// recursively process element keys
$rval = new stdClass();
foreach($element as $key => $value) {
// compact @id and @type(s)
if($key === '@id' || $key === '@type') {
// compact single @id
if(is_string($value)) {
$value = $this->_compactIri($ctx, $value);
}
// value must be a @type array
else {
$types = array();
foreach($value as $v) {
$types[] = $this->_compactIri($ctx, $v);
}
$value = $types;
}
// compact property and add value
$prop = $this->_compactIri(ctx, key);
$isArray = (is_array($value) && count($value) === 0);
self::addValue($rval, $prop, $value, $isArray);
continue;
}
// Note: value must be an array due to expansion algorithm.
// preserve empty arrays
if(count($value) === 0) {
$prop =$this->_compactIri($ctx, $key);
self::addValue($rval, $prop, array(), true);
}
// recusively process array values
foreach($value as $v) {
$isList = self::_isListValue($v);
// compact property
$prop = $this->_compactIri($ctx, $key, $v);
// remove @list for recursion (will be re-added if necessary)
if($isList) {
$v = $v->{'@list'};
}
// recursively compact value
$v = $this->compact($ctx, $prop, $v, $options);
// get container type for property
$container = self::getContextValue($ctx, $prop, '@container');
// handle @list
if($isList && $container !== '@list') {
// handle messy @list compaction
if(property_exists($rval, $prop) && $options['strict']) {
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');
}
// reintroduce @list keyword
$kwlist = $this->_compactIri($ctx, '@list');
$val = new stdClass();
$val->{$kwlist} = $v;
$v = $val;
}
// if @container is @set or @list or value is an empty array, use
// an array when adding value
$isArray = ($container === '@set' || $container === '@list' ||
(is_array($v) && count($v) === 0));
// add compact value
self::addValue($rval, $prop, $v, $isArray);
}
}
return $rval;
}
}
/**
* Recursively increments the relation serialization for a mapping.
*
* @param mb the mapping builder to update.
*/
public function serializeMapping($mb)
{
if(count($mb->keyStack) > 0)
{
// continue from top of key stack
$next = array_pop($mb->keyStack);
$len = count($next->keys);
for(; $next->idx < $len; ++$next->idx)
{
$k = $next->keys[$next->idx];
if(!property_exists($mb->adj, $k))
{
$mb->keyStack[] = $next;
break;
}
// only primitives remain which are already compact
return $element;
}
if(property_exists($mb->done, $k))
{
// mark cycle
$mb->s .= '_' . $k;
}
else
{
// mark key as serialized
$mb->done->$k = true;
// serialize top-level key and its details
$s = $k;
$adj = $mb->adj->$k;
$iri = $adj->i;
if(property_exists($this->subjects, $iri))
{
$b = $this->subjects->$iri;
// serialize properties
$s .= '[' . $this->serializeProperties($b) . ']';
// serialize references
$s .= '[';
$first = true;
$refs = $this->edges->refs->$iri->all;
foreach($refs as $r)
{
if($first)
{
$first = false;
}
else
{
$s .= '|';
}
$s .= '<' . $r->p . '>';
$s .= _isBlankNodeIri($r->s) ?
'_:' : ('<' . $refs->s . '>');
}
$s .= ']';
}
// serialize adjacent node keys
$s .= implode($adj->k);
$mb->s .= $s;
$entry = new stdClass();
$entry->keys = $adj->k;
$entry->idx = 0;
$mb->keyStack[] = $entry;
$this->serializeMapping($mb);
}
}
}
}
/**
* Recursively serializes adjacent bnode combinations.
*
* @param s the serialization to update.
* @param iri the IRI of the bnode being serialized.
* @param siri the serialization name for the bnode IRI.
* @param mb the MappingBuilder to use.
* @param dir the edge direction to use ('props' or 'refs').
* @param mapped all of the already-mapped adjacent bnodes.
* @param notMapped all of the not-yet mapped adjacent bnodes.
*/
public function serializeCombos(
$s, $iri, $siri, $mb, $dir, $mapped, $notMapped)
{
// handle recursion
if(count($notMapped) > 0)
{
// copy mapped nodes
$mapped = _clone($mapped);
// map first bnode in list
$mapped->{$mb->mapNode($notMapped[0]->s)} = $notMapped[0]->s;
// recurse into remaining possible combinations
$original = $mb->copy();
$notMapped = array_slice($notMapped, 1);
$rotations = max(1, count($notMapped));
for($r = 0; $r < $rotations; ++$r)
{
$m = ($r === 0) ? $mb : $original->copy();
$this->serializeCombos(
$s, $iri, $siri, $m, $dir, $mapped, $notMapped);
// rotate not-mapped for next combination
_rotate($notMapped);
}
}
// no more adjacent bnodes to map, update serialization
else
{
$keys = array_keys((array)$mapped);
sort($keys);
$entry = new stdClass();
$entry->i = $iri;
$entry->k = $keys;
$entry->m = $mapped;
$mb->adj->$siri = $entry;
$this->serializeMapping($mb);
// optimize away mappings that are already too large
if($s->$dir === null or
_compareSerializations($mb->s, $s->$dir->s) <= 0)
{
// recurse into adjacent values
foreach($keys as $i => $k)
{
$this->serializeBlankNode($s, $mapped->$k, $mb, $dir);
}
// update least serialization if new one has been found
$this->serializeMapping($mb);
if($s->$dir === null or
(_compareSerializations($mb->s, $s->$dir->s) <= 0 and
strlen($mb->s) >= strlen($s->$dir->s)))
{
$s->$dir = new stdClass();
$s->$dir->s = $mb->s;
$s->$dir->m = $mb->mapping;
}
}
}
}
/**
* Computes the relation serialization for the given blank node IRI.
*
* @param s the serialization to update.
* @param iri the current bnode IRI to be mapped.
* @param mb the MappingBuilder to use.
* @param dir the edge direction to use ('props' or 'refs').
*/
public function serializeBlankNode($s, $iri, $mb, $dir)
{
// only do mapping if iri not already processed
if(!property_exists($mb->processed, $iri))
{
// iri now processed
$mb->processed->$iri = true;
$siri = $mb->mapNode($iri);
// copy original mapping builder
$original = $mb->copy();
// split adjacent bnodes on mapped and not-mapped
$adjs = $this->edges->$dir->$iri->bnodes;
$mapped = new stdClass();
$notMapped = array();
foreach($adjs as $adj)
{
if(property_exists($mb->mapping, $adj->s))
{
$mapped->{$mb->mapping->{$adj->s}} = $adj->s;
}
else
{
$notMapped[] = $adj;
}
}
// TODO: ensure this optimization does not alter canonical order
// if the current bnode already has a serialization, reuse it
/*$hint = property_exists($this->serializations, $iri) ?
$this->serializations->$iri->$dir : null;
if($hint !== null)
{
$hm = $hint->m;
usort(notMapped,
{
return _compare(hm[a.s], hm[b.s]);
});
for($i in notMapped)
{
mapped[mb.mapNode(notMapped[i].s)] = notMapped[i].s;
}
notMapped = array();
}*/
// loop over possible combinations
$combos = max(1, count($notMapped));
for($i = 0; $i < $combos; ++$i)
{
$m = ($i === 0) ? $mb : $original->copy();
$this->serializeCombos(
$s, $iri, $siri, $mb, $dir, $mapped, $notMapped);
}
}
}
/**
* Compares two blank nodes for equivalence.
*
* @param a the first blank node.
* @param b the second blank node.
*
* @return -1 if a < b, 0 if a == b, 1 if a > b.
*/
public function deepCompareBlankNodes($a, $b)
{
$rval = 0;
// compare IRIs
$iriA = $a->{'@id'};
$iriB = $b->{'@id'};
if($iriA === $iriB)
{
$rval = 0;
}
else
{
// do shallow compare first
$rval = $this->shallowCompareBlankNodes($a, $b);
// deep comparison is necessary
if($rval === 0)
{
// compare property edges and then reference edges
$dirs = array('props', 'refs');
for($i = 0; $rval === 0 and $i < 2; ++$i)
{
// recompute 'a' and 'b' serializations as necessary
$dir = $dirs[$i];
$sA = $this->serializations->$iriA;
$sB = $this->serializations->$iriB;
if($sA->$dir === null)
{
$mb = new MappingBuilder();
if($dir === 'refs')
{
// keep same mapping and count from 'props' serialization
$mb->mapping = _clone($sA->props->m);
$mb->count = count(array_keys((array)$mb->mapping)) + 1;
}
$this->serializeBlankNode($sA, $iriA, $mb, $dir);
}
if($sB->$dir === null)
{
$mb = new MappingBuilder();
if($dir === 'refs')
{
// keep same mapping and count from 'props' serialization
$mb->mapping = _clone($sB->props->m);
$mb->count = count(array_keys((array)$mb->mapping)) + 1;
}
$this->serializeBlankNode($sB, $iriB, $mb, $dir);
}
// compare serializations
$rval = _compare($sA->$dir->s, $sB->$dir->s);
}
}
}
return $rval;
}
/**
* Performs a shallow sort comparison on the given bnodes.
*
* @param a the first bnode.
* @param b the second bnode.
*
* @return -1 if a < b, 0 if a == b, 1 if a > b.
*/
public function shallowCompareBlankNodes($a, $b)
{
$rval = 0;
/* ShallowSort Algorithm (when comparing two bnodes):
1. Compare the number of properties.
1.1. The bnode with fewer properties is first.
2. Compare alphabetically sorted-properties.
2.1. The bnode with the alphabetically-first property is first.
3. For each property, compare object values.
4. Compare the number of references.
4.1. The bnode with fewer references is first.
5. Compare sorted references.
5.1. The bnode with the reference iri (vs. bnode) is first.
5.2. The bnode with the alphabetically-first reference iri is first.
5.3. The bnode with the alphabetically-first reference property is
first.
*/
$pA = array_keys((array)$a);
$pB = array_keys((array)$b);
// step #1
$rval = _compare(count($pA), count($pB));
// step #2
if($rval === 0)
{
sort($pA);
sort($pB);
$rval = _compare($pA, $pB);
}
// step #3
if($rval === 0)
{
$rval = _compareBlankNodeObjects($a, $b);
}
// step #4
if($rval === 0)
{
$edgesA = $this->edges->refs->{$a->{'@id'}}->all;
$edgesB = $this->edges->refs->{$b->{'@id'}}->all;
$edgesALen = count($edgesA);
$rval = _compare($edgesALen, count($edgesB));
}
// step #5
if($rval === 0)
{
for($i = 0; $i < $edgesALen and $rval === 0; ++$i)
{
$rval = $this->compareEdges($edgesA[$i], $edgesB[$i]);
}
}
return $rval;
}
/**
* Compares two edges. Edges with an IRI (vs. a bnode ID) come first, then
* alphabetically-first IRIs, then alphabetically-first properties. If a
* blank node has been canonically named, then blank nodes will be compared
* after properties (with a preference for canonically named over
* non-canonically named), otherwise they won't be.
*
* @param a the first edge.
* @param b the second edge.
*
* @return -1 if a < b, 0 if a == b, 1 if a > b.
*/
public function compareEdges($a, $b)
{
$rval = 0;
$bnodeA = _isBlankNodeIri($a->s);
$bnodeB = _isBlankNodeIri($b->s);
$c14n = $this->ng->c14n;
// if not both bnodes, one that is a bnode is greater
if($bnodeA != $bnodeB)
{
$rval = $bnodeA ? 1 : -1;
}
else
{
if(!$bnodeA)
{
$rval = _compare($a->s, $b->s);
}
if($rval === 0)
{
$rval = _compare($a->p, $b->p);
}
// do bnode IRI comparison if canonical naming has begun
if($rval === 0 and $c14n !== null)
{
$c14nA = $c14n->inNamespace($a->s);
$c14nB = $c14n->inNamespace($b->s);
if($c14nA != $c14nB)
{
$rval = $c14nA ? 1 : -1;
}
else if($c14nA)
{
$rval = _compare($a->s, $b->s);
}
}
}
return $rval;
}
/**
* Populates the given reference map with all of the subject edges in the
* graph. The references will be categorized by the direction of the edges,
* where 'props' is for properties and 'refs' is for references to a subject
* as an object. The edge direction categories for each IRI will be sorted
* into groups 'all' and 'bnodes'.
*/
public function collectEdges()
{
$refs = $this->edges->refs;
$props = $this->edges->props;
// collect all references and properties
foreach($this->subjects as $iri => $subject)
{
foreach($subject as $key => $object)
{
if($key !== '@id')
{
// normalize to array for single codepath
$tmp = !is_array($object) ? array($object) : $object;
foreach($tmp as $o)
{
if(is_object($o) and property_exists($o, '@id') and
property_exists($this->subjects, $o->{'@id'}))
{
$objIri = $o->{'@id'};
// map object to this subject
$e = new stdClass();
$e->s = $iri;
$e->p = $key;
$refs->$objIri->all[] = $e;
// map this subject to object
$e = new stdClass();
$e->s = $objIri;
$e->p = $key;
$props->$iri->all[] = $e;
}
}
}
}
}
// create sorted categories
foreach($refs as $iri => $ref)
{
usort($ref->all, array($this, 'compareEdges'));
$ref->bnodes = array_filter($ref->all, '_filterBlankNodeEdges');
}
foreach($props as $iri => $prop)
{
usort($prop->all, array($this, 'compareEdges'));
$prop->bnodes = array_filter($prop->all, '_filterBlankNodeEdges');
}
}
}
/**
* Returns true if the given input is a subject and has one of the given types
* in the given frame.
*
* @param input the input.
* @param frame the frame with types to look for.
*
* @return true if the input has one of the given types.
*/
function _isType($input, $frame)
{
$rval = false;
// check if type(s) are specified in frame and input
$type = '@type';
if(property_exists($frame, $type) and
is_object($input) and
property_exists($input, $type))
{
$tmp = is_array($input->$type) ? $input->$type : array($input->$type);
$types = is_array($frame->$type) ? $frame->$type : array($frame->$type);
$length = count($types);
for($t = 0; $t < $length and !$rval; ++$t)
{
$type = $types[$t];
foreach($tmp as $e)
{
if($e === $type)
{
$rval = true;
break;
}
}
}
}
return $rval;
}
/**
* Filters non-keywords.
*
* @param e the element to check.
*
* @return true if the element is a non-keyword.
*/
function _filterNonKeyWords($e)
{
return strpos($e, '@') !== 0;
}
/**
* Returns true if the given input matches the given frame via duck-typing.
*
* @param input the input.
* @param frame the frame to check against.
*
* @return true if the input matches the frame.
*/
function _isDuckType($input, $frame)
{
$rval = false;
// frame must not have a specific type
if(!property_exists($frame, '@type'))
{
// get frame properties that must exist on input
$props = array_filter(array_keys((array)$frame), '_filterNonKeywords');
if(count($props) === 0)
{
// input always matches if there are no properties
$rval = true;
}
// input must be a subject with all the given properties
else if(is_object($input) and property_exists($input, '@id'))
{
$rval = true;
foreach($props as $prop)
{
if(!property_exists($input, $prop))
{
$rval = false;
break;
}
}
}
}
return $rval;
}
/**
* Recursively removes dependent dangling embeds.
*
* @param iri the iri of the parent to remove embeds for.
* @param embeds the embeds map.
*/
function removeDependentEmbeds($iri, $embeds)
{
$iris = get_object_vars($embeds);
foreach($iris as $i => $embed)
{
if($embed->parent !== null and
$embed->parent->{'@id'} === $iri)
{
unset($embeds->$i);
removeDependentEmbeds($i, $embeds);
}
}
}
/**
* Subframes a value.
*
* @param subjects a map of subjects in the graph.
* @param value the value to subframe.
* @param frame the frame to use.
* @param embeds a map of previously embedded subjects, used to prevent cycles.
* @param autoembed true if auto-embed is on, false if not.
* @param parent the parent object.
* @param parentKey the parent object.
* @param options the framing options.
*
* @return the framed input.
*/
function _subframe(
$subjects, $value, $frame, $embeds, $autoembed,
$parent, $parentKey, $options)
{
// get existing embed entry
$iri = $value->{'@id'};
$embed = property_exists($embeds, $iri) ? $embeds->{$iri} : null;
// determine if value should be embedded or referenced,
// embed is ON if:
// 1. The frame OR default option specifies @embed as ON, AND
// 2. There is no existing embed OR it is an autoembed, AND
// autoembed mode is off.
$embedOn = (
((property_exists($frame, '@embed') and $frame->{'@embed'}) or
(!property_exists($frame, '@embed') and $options->defaults->embedOn)) and
($embed === null or ($embed->autoembed and !$autoembed)));
if(!$embedOn)
{
// not embedding, so only use subject IRI as reference
$tmp = new stdClass();
$tmp->{'@id'} = $value->{'@id'};
$value = $tmp;
}
else
{
// create new embed entry
if($embed === null)
{
$embed = new stdClass();
$embeds->{$iri} = $embed;
}
// replace the existing embed with a reference
else if($embed->parent !== null)
{
if(is_array($embed->parent->{$embed->key}))
{
// find and replace embed in array
$arrLen = count($embed->parent->{$embed->key});
for($i = 0; $i < $arrLen; ++$i)
{
$obj = $embed->parent->{$embed->key}[$i];
if(is_object($obj) and property_exists($obj, '@id') and
$obj->{'@id'} === $iri)
{
$tmp = new stdClass();
$tmp->{'@id'} = $value->{'@id'};
$embed->parent->{$embed->key}[$i] = $tmp;
break;
}
}
}
else
{
$tmp = new stdClass();
$tmp->{'@id'} = $value->{'@id'};
$embed->parent->{$embed->key} = $tmp;
}
// recursively remove any dependent dangling embeds
removeDependentEmbeds($iri, $embeds);
}
// update embed entry
$embed->autoembed = $autoembed;
$embed->parent = $parent;
$embed->key = $parentKey;
// check explicit flag
$explicitOn = property_exists($frame, '@explicit') ?
$frame->{'@explicit'} : $options->defaults->explicitOn;
if($explicitOn)
{
// remove keys from the value that aren't in the frame
foreach($value as $key => $v)
{
// do not remove @id or any frame key
if($key !== '@id' and !property_exists($frame, $key))
{
unset($value->$key);
}
}
}
// iterate over keys in value
$vars = get_object_vars($value);
foreach($vars as $key => $v)
{
// skip keywords
if(strpos($key, '@') !== 0)
{
// get the subframe if available
if(property_exists($frame, $key))
{
$f = $frame->{$key};
$_autoembed = false;
}
// use a catch-all subframe to preserve data from graph
else
{
$f = is_array($v) ? array() : new stdClass();
$_autoembed = true;
}
// build input and do recursion
$input = is_array($v) ? $v : array($v);
$length = count($input);
for($n = 0; $n < $length; ++$n)
{
// replace reference to subject w/embedded subject
if(is_object($input[$n]) and
property_exists($input[$n], '@id') and
property_exists($subjects, $input[$n]->{'@id'}))
{
$input[$n] = $subjects->{$input[$n]->{'@id'}};
}
}
$value->$key = _frame(
$subjects, $input, $f, $embeds, $_autoembed,
$value, $key, $options);
}
}
// iterate over frame keys to add any missing values
foreach($frame as $key => $f)
{
// skip keywords and non-null keys in value
if(strpos($key, '@') !== 0 and
(!property_exists($value, $key) || $value->{$key} === null))
{
// add empty array to value
if(is_array($f))
{
// add empty array/null property to value
$value->$key = array();
}
// add default value to value
else
{
// use first subframe if frame is an array
if(is_array($f))
{
$f = (count($f) > 0) ? $f[0] : new stdClass();
}
// determine if omit default is on
$omitOn = property_exists($f, '@omitDefault') ?
$f->{'@omitDefault'} :
$options->defaults->omitDefaultOn;
if(!$omitOn)
{
if(property_exists($f, '@default'))
{
// use specified default value
$value->{$key} = $f->{'@default'};
}
else
{
// build-in default value is: null
$value->{$key} = null;
}
}
}
}
}
}
return $value;
}
/**
* Recursively frames the given input according to the given frame.
*
* @param subjects a map of subjects in the graph.
* @param input the input to frame.
* @param frame the frame to use.
* @param embeds a map of previously embedded subjects, used to prevent cycles.
* @param autoembed true if auto-embed is on, false if not.
* @param parent the parent object (for subframing), null for none.
* @param parentKey the parent key (for subframing), null for none.
* @param options the framing options.
*
* @return the framed input.
*/
function _frame(
$subjects, $input, $frame, $embeds, $autoembed,
$parent, $parentKey, $options)
{
$rval = null;
// prepare output, set limit, get array of frames
$limit = -1;
if(is_array($frame))
{
/**
* Recursively expands an element using the given context. Any context in
* the element will be removed. All context URLs must have been resolved
* before calling this method.
*
* @param stdClass $ctx the context to use.
* @param mixed $property the property for the element, null for none.
* @param mixed $element the element to expand.
* @param array $options the expansion options.
* @param bool $propertyIsList true if the property is a list, false if not.
*
* @return mixed the expanded value.
*/
protected function _expand(
$ctx, $property, $element, $options, $propertyIsList) {
// recursively expand array
if(is_array($element)) {
$rval = array();
$frames = $frame;
if(count($frames) == 0)
{
$frames[] = new stdClass();
foreach($element as $e) {
// expand element
$e = $this->_expand($ctx, $property, $e, $options, $propertyIsList);
if(is_array($e) && $propertyIsList) {
// 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) {
$rval[] = $e;
}
}
}
else
{
$frames = array($frame);
$limit = 1;
}
return $rval;
}
// iterate over frames adding input matches to list
$frameLen = count($frames);
$values = array();
for($i = 0; $i < $frameLen and $limit !== 0; ++$i)
{
// get next frame
$frame = $frames[$i];
if(!is_object($frame))
{
throw new Exception(
'Invalid JSON-LD frame. Frame type is not a map or array.');
// recursively expand object
if(is_object($element)) {
// if element has a context, process it
if(property_exists($element, '@context')) {
$ctx = $this->_processContext($ctx, $element->{'@context'}, $options);
unset($element->{'@context'});
}
// create array of values for each frame
$inLen = count($input);
$v = array();
for($n = 0; $n < $inLen and $limit !== 0; ++$n)
{
// dereference input if it refers to a subject
$next = $input[$n];
if(is_object($next) and property_exists($next, '@id') and
property_exists($subjects, $next->{'@id'}))
{
$next = $subjects->{$next->{'@id'}};
}
$rval = new stdClass();
foreach($element as $key => $value) {
// expand property
$prop = $this->_expandTerm($ctx, $key);
// add input to list if it matches frame specific type or duck-type
if(_isType($next, $frame) or _isDuckType($next, $frame))
{
$v[] = $next;
--$limit;
}
}
$values[$i] = $v;
}
// drop non-absolute IRI keys that aren't keywords
if(!self::_isAbsoluteIri($prop) && !self::_isKeyword($prop, $ctx)) {
continue;
}
// for each matching value, add it to the output
$vaLen = count($values);
for($i1 = 0; $i1 < $vaLen; ++$i1)
{
foreach($values[$i1] as $value)
{
$frame = $frames[$i1];
// if value is null and property is not @value, continue
$value = $element->{$key};
if($value === null && $prop !== '@value') {
continue;
}
// if value is a subject, do subframing
if(_isSubject($value))
{
$value = _subframe(
$subjects, $value, $frame, $embeds, $autoembed,
$parent, $parentKey, $options);
}
// syntax error if @id is not a string
if($prop === '@id' && !is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@id" value must a string.',
'jsonld.SyntaxError', array('value' => $value));
}
// add value to output
if($rval === null)
{
$rval = $value;
}
else
{
// determine if value is a reference to an embed
$isRef = (_isReference($value) and
property_exists($embeds, $value->{'@id'}));
// @type must be a string, array of strings, or an empty JSON object
if($prop === '@type' &&
!(is_string($value) || self::_isArrayOfStrings($value) ||
self::_isEmptyObject($value))) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@type" value must a string, an array ' +
'of strings, or an empty object.',
'jsonld.SyntaxError', array('value' => $value));
}
// push any value that isn't a parentless reference
if(!($parent === null and $isRef))
{
$rval[] = $value;
// @graph must be an array or an object
if($prop === '@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($prop === '@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($prop === '@language' && !is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@language" value must not be a string.',
'jsonld.SyntaxError', array('value' => $value));
}
// recurse into @list, @set, or @graph, keeping the active property
$isList = ($prop === '@list');
if($isList || $prop === '@set' || $prop === '@graph') {
$value = $this->_expand($ctx, $property, $value, $options, $isList);
if($isList && self::_isListValue($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; lists of lists are not permitted.',
'jsonld.SyntaxError');
}
}
else {
// update active property and recursively expand value
$property = $key;
$value = $this->_expand($ctx, $property, $value, $options, false);
}
// drop null values if property is not @value (dropped below)
if($value !== null || $prop === '@value') {
// convert value to @list if container specifies it
if($prop !== '@list' && !self::_isListValue($value)) {
$container = self::getContextValue($ctx, $property, '@container');
if($container === '@list') {
// ensure value is an array
$value = self::arrayify();
$value = (object)array('@list' => $value);
}
}
}
}
}
return $rval;
// add value, use an array if not @id, @type, @value, or @language
$useArray = !($prop === '@id' || $prop === '@type' ||
$prop === '@value' || $prop === '@language');
self::addValue($rval, $prop, $value, $useArray);
}
}
// get property count on expanded output
$count = count(get_object_vars($rval));
// @value must only have @language or @type
if(property_exists($rval, '@value')) {
if(($count === 2 && !property_exists($rval, '@type') &&
!property_exists($rval, '@language')) ||
$count > 2) {
throw new JsonLdException(
'Invalid JSON-LD syntax; an element containing "@value" must have ' +
'at most one other property which can be "@type" or "@language".',
'jsonld.SyntaxError', array('element' => $rval));
}
// value @type must be a string
if(property_exists($rval, '@type') && !is_string($rval->{'@type'})) {
throw new JsonLdException(
'Invalid JSON-LD syntax; the "@type" value of an element ' +
'containing "@value" must be a string.',
'jsonld.SyntaxError', array('element' => $rval));
}
// return only the value of @value if there is no @type or @language
else if($count === 1) {
$rval = $rval->{'@value'};
}
// drop null @values
else if($rval->{'@value'} === null) {
$rval = null;
}
}
// 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) {
throw new JsonLdException(
'Invalid JSON-LD syntax; if an element has the property "@set" ' +
'or "@list", then it must be its only property.',
'jsonld.SyntaxError', array('element' => $rval));
}
// optimize away @set
if(property_exists($rval, '@set')) {
$rval = $rval->{'@set'};
}
}
// drop objects with only @language
else if(property_exists($rval, '@language') && $count === 1) {
$rval = null;
}
return $rval;
}
// expand element according to value expansion rules
return $this->_expandValue($ctx, $property, $element, $options['base']);
}
/**
* Performs JSON-LD framing.
*
* @param array $input the expanded JSON-LD to frame.
* @param array $frame the expanded JSON-LD frame to use.
* @param array $options the framing options.
*
* @return array the framed output.
*/
protected function _frame($input, $frame, $options) {
// create framing state
$state = (object)array(
'options' => $options,
'subjects' => new stdClass());
// produce a map of all subjects and name each bnode
$namer = new UniqueNamer('_:t');
$this->_flatten($state->subjects, $input, $namer, null, null);
// frame the subjects
$framed = new ArrayObject();
$this->_match_frame($state, $state->subjects, $frame, $framed, null);
return (array)$framed;
}
/**
* Performs JSON-LD normalization.
*
* @param input the expanded JSON-LD object to normalize.
*
* @return the normalized output.
*/
protected function _normalize($input) {
// get statements
$namer = new UniqueNamer('_:t');
$bnodes = new stdClass();
$subjects = new stdClass();
$this->_getStatements($input, $namer, $bnodes, $subjects);
// create canonical namer
$namer = new UniqueNamer('_:c14n');
// continue to hash bnode statements while bnodes are assigned names
$unnamed = null;
$nextUnnamed = get_object_vars($bnodes);
$duplicates = null;
do {
$unnamed = nextUnnamed;
$nextUnnamed = array();
$duplicates = new stdClass();
$unique = new stdClass();
foreach($unnamed as $bnode) {
// hash statements for each unnamed bnode
$statements = $bnodes->{$bnode};
$hash = $this->_hashStatements($statements, $namer);
// store hash as unique or a duplicate
if(property_exists($duplicate, $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 = get_object_vars($unique);
sort($hashes);
foreach($hashes as $hash) {
$bnode = $unique->{$hash};
$namer->getName($bnode);
}
}
while(count($unnamed) > count($nextUnnamed));
// enumerate duplicate hash groups in sorted order
$hashes = get_object_vars($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('_:t');
$path_namer->getName($bnode);
$results[] = $this->_hashPaths(
$bnodes, $bnodes->{$bnode}, $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 JSON-LD array
$output = array();
// add all bnodes
foreach($bnodes as $id => $statements) {
// add all property statements to bnode
$name = $namer->getName($id);
$bnode = (object)array('@id' => $name);
foreach($statements as $statement) {
if($statement->s === '_:a') {
$z = $this->_getBlankNodeName($statement->o);
$o = $z ? (object)array('@id' => $namer.getName($z)) : $statement->o;
self::addValue($bnode, $statement->p, $o, true);
}
}
$output[] = $bnode;
}
// add all non-bnodes
foreach($subjects as $id => $statements) {
// add all statements to subject
$subject = (object)array('@id' => $id);
foreach($statements as $statement) {
$z = $this->_getBlankNodeName($statement->o);
$o = $z ? (object)array('@id' => $namer.getName($z)) : $statement->o;
self::addValue($subject, $statement->p, $o, true);
}
$output[] = $subject;
}
// sort normalized output by @id
usort($output, function($a, $b) {
$a = $a->{'@id'};
$b = $b->{'@id'};
return ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
});
return $output;
}
/**
* Outputs the RDF statements found in the given JSON-LD object.
*
* @param mixed $input the JSON-LD object.
*
* @return array the RDF statements.
*/
protected function _toRDF($input) {
// FIXME: implement
throw new JsonLdException('Not implemented', 'jsonld.NotImplemented');
}
/**
* 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 array $options the context processing options.
*
* @return stdClass the new active context.
*/
protected function _processContext($active_ctx, $local_ctx, $options) {
// initialize the resulting context
$rval = self::copy($active_ctx);
// normalize local context to an array
$ctxs = self::arrayify($local_ctx);
// process each context in order
foreach($ctxs as $ctx) {
// reset to initial context
if($ctx === null) {
$rval = $this->_getInitialContext();
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 resolved before this call
if(!is_object($ctx)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context must be an object.',
'jsonld.SyntaxError', array('context' => $ctx));
}
// define context mappings for keys in local context
$defined = new stdClass();
foreach($ctx as $k => $v) {
$this->_defineContextMapping(
$rval, $ctx, $k, $options['base'], $defined);
}
}
return $rval;
}
/**
* Expands the given value by using the coercion and keyword rules in the
* given context.
*
* @param stdClass $ctx the active context to use.
* @param string $property the property the value is associated with.
* @param mixed $value the value to expand.
* @param string $base the base IRI to use.
*
* @return mixed the expanded value.
*/
protected function _expandValue($ctx, $property, $value, $base) {
// default to simple string return value
$rval = $value;
// special-case expand @id and @type (skips '@id' expansion)
$prop = $this->_expandTerm($ctx, $property);
if($prop === '@id') {
$rval = $this->_expandTerm($ctx, $value, $base);
}
else if($prop === '@type') {
$rval = $this->_expandTerm($ctx, $value);
}
else {
// get type definition from context
$type = self::getContextValue($ctx, $property, '@type');
// do @id expansion
if($type === '@id') {
$rval = (object)array('@id' => $this->_expandTerm($ctx, $value, $base));
}
// other type
else if($type !== null) {
$rval = (object)array('@value' => strval($value), '@type' => $type);
}
// check for language tagging
else {
$language = self::getContextValue($ctx, $property, '@language');
if($language !== null) {
$rval = (object)array(
'@value' => strval(value), '@language' => $language);
}
}
}
return $rval;
}
/**
* Recursively gets all statements from the given expanded JSON-LD input.
*
* @param mixed $input the valid expanded JSON-LD input.
* @param UniqueNamer $namer the namer to use when encountering blank nodes.
* @param stdClass $bnodes the blank node statements map to populate.
* @param stdClass $subjects the subject statements map to populate.
* @param mixed [$name] the name (@id) assigned to the current input.
*/
protected function _getStatements(
$input, $namer, $bnodes, $subjects, $name=null) {
// recurse into arrays
if(is_array($input)) {
foreach($input as $e) {
$this->_getStatements($e, $namer, $bnodes, $subjects);
}
}
// safe to assume input is a subject/blank node
else {
$isBnode = self::_isBlankNode($input);
// name blank node if appropriate, use passed name if given
if($name === null) {
if(property_exists($input, '@id')) {
$name = $input->{'@id'};
}
if($isBnode) {
$name = $namer->getName($name);
}
}
// use a subject of '_:a' for blank node statements
$s = $isBnode ? '_:a' : $name;
// get statements for the blank node
$entries;
if($isBnode) {
if(!property_exists($bnodes, $name)) {
$entries = $bnodes->{$name} = new ArrayObject();
}
else {
$entries = $bnodes->{$name};
}
}
else if(!property_exists($subjects, $name)) {
$entries = $subjects->{$name} = new ArrayObject();
}
else {
$entries = $subjects->{$name};
}
// add all statements in input
foreach($input as $p => $objects) {
// skip @id
if($p === '@id') {
continue;
}
// convert @lists into embedded blank node linked lists
foreach($objects as $i => $o) {
if(self::_isListValue($o)) {
$objects[$i] = $this->_makeLinkedList($o);
}
}
foreach($objects as $o) {
// convert boolean to @value
if(is_bool($o)) {
$o = (object)array(
'@value' => strval($o), '@type' => self::XSD_BOOLEAN);
}
// convert double to @value
else if(is_double($o)) {
// do special JSON-LD double format, printf('%1.16e') equivalent
$o = preg_replace('/(e(?:\+|-))([0-9])$/', '${1}0${2}',
sprintf('%1.16e', $o));
$o = (object)array('@value' => $o, '@type' => self::XSD_DOUBLE);
}
// convert integer to @value
else if(is_numeric($o)) {
$o = (object)array(
'@value' => strval($o), '@type' => self::XSD_INTEGER);
}
// object is a blank node
if(self::_isBlankNode($o)) {
// name object position blank node
$o_name = $namer->getName($o->{'@id'});
// add property statement
$this->_addStatement($entries, (object)array(
's' => $s, 'p' => $p, 'o' => (object)array('@id' => $o_name)));
// add reference statement
if(!property_exists($bnodes, $name)) {
$o_entries = $bnodes->{$o_name} = new ArrayObject();
}
else {
$o_entries = $bnodes->{$o_name};
}
$this->_addStatement(
$o_entries, (object)array(
's' => $name, 'p' => $p, 'o' => (object)array('@id' => '_:a')));
// recurse into blank node
$this->_getStatements($o, $namer, $bnodes, $subjects, $o_name);
}
// object is a string, @value, subject reference
else if(is_string($o) || self::_isValue($o) ||
self::_isSubjectReference($o)) {
// add property statement
$this->_addStatement($entries, (object)array(
's' => $s, 'p' => $p, 'o' => $o));
// ensure a subject entry exists for subject reference
if(self::_isSubjectReference($o) &&
!property_exists($subjects, $name)) {
$subjects->{$name} = new ArrayObject();
}
}
// object must be an embedded subject
else {
// add property statement
$this->_addStatement($entries, (object)array(
's' => $s, 'p' => $p, 'o' => (object)array(
'@id' => $o->{'@id'})));
// recurse into subject
$this->_getStatements($o, $namer, $bnodes, $subjects);
}
}
}
}
}
/**
* Converts a @list value into an embedded linked list of blank nodes in
* expanded form. The resulting array can be used as an RDF-replacement for
* a property that used a @list.
*
* @param array $value the @list value.
*
* @return stdClass the head of the linked list of blank nodes.
*/
protected function _makeLinkedList($value) {
// convert @list array into embedded blank node linked list
$list = $value->{'@list'};
// build linked list in reverse
$len = count($list);
$tail = (object)array('@id' => self::RDF_NIL);
for($i = $len - 1; $i >= 0; --$i) {
$tail = (object)array(
self::RDF_FIRST => array($list[$i]),
self::RDF_REST => array($tail));
}
return $tail;
}
/**
* Adds a statement to an array of statements. If the statement already exists
* in the array, it will not be added.
*
* @param ArrayObject $statements the statements array.
* @param stdClass $statement the statement to add.
*/
protected function _addStatement($statements, $statement) {
foreach($statements as $s) {
if($s->s === $statement->s && $s->p === $statement->p &&
self::compareValues($s->o, $statement->o)) {
return;
}
}
$statements[] = $statement;
}
/**
* Hashes all of the statements about a blank node.
*
* @param ArrayObject $statements the statements about the bnode.
* @param UniqueNamer $namer the canonical bnode namer.
*
* @return string the new hash.
*/
protected function _hashStatements($statements, $namer) {
// serialize all statements
$triples = array();
foreach($statements as $statement) {
// serialize triple
$triple = '';
// serialize subject
if($statement->s === '_:a') {
$triple .= '_:a';
}
else if(strpos($statement->s, '_:') === 0) {
$id = $statement->s;
$id = $namer->isNamed($id) ? $namer->getName($id) : '_:z';
$triple .= $id;
}
else {
$triple .= '<' . $statement->s . '>';
}
// serialize property
$p = ($statement->p === '@type') ? self::RDF_TYPE : $statement->p;
$triple .= ' <' . $p . '> ';
// serialize object
if(self::_isBlankNode($statement->o)) {
if($statement->o->{'@id'} === '_:a') {
$triple .= '_:a';
}
else {
$id = $statement->o->{'@id'};
$id = $namer->isNamed($id) ? $namer->getName($id) : '_:z';
$triple .= $id;
}
}
else if(is_string($statement->o)) {
$triple .= '"' . $statement->o . '"';
}
else if(self::_isSubjectReference($statement->o)) {
$triple .= '<' . $statement->o->{'@id'} . '>';
}
// must be a value
else {
$triple .= '"' . $statement->o->{'@value'} . '"';
if(property_exists($statement->o, '@type')) {
$triple .= '^^<' . $statement->o['@type'] . '>';
}
else if(property_exists($statement->o, '@language')) {
$triple .= '@' . $statement->o{'@language'};
}
}
// add triple
$triples[] = $triple;
}
// sort serialized triples
sort($triples);
// return hashed triples
return sha1(implode($triples));
}
/**
* Produces a hash for the paths of adjacent bnodes for a bnode,
* incorporating all information about its subgraph of bnodes. This
* method will recursively pick adjacent bnode permutations that produce the
* lexicographically-least 'path' serializations.
*
* @param stdClass $bnodes the map of bnode statements.
* @param ArrayObject $statements the statements for the bnode to produce
* the hash for.
* @param UniqueNamer $namer the canonical bnode namer.
* @param UniqueNamer $path_namer the namer used to assign names to adjacent
* bnodes.
*
* @return stdClass the hash and path namer used.
*/
protected function _hashPaths($bnodes, $statements, $namer, $path_namer) {
// create SHA-1 digest
$md = hash_init('sha1');
// group adjacent bnodes by hash, keep properties and references separate
$groups = new stdClass();
foreach($statements as $statement) {
$bnode = null;
$direction = null;
if($statement->s !== '_:a' && strpos($statement->s, '_:') === 0) {
$bnode = $statement->s;
$direction = 'p';
}
else {
$bnode = $this->_getBlankNodeName($statement->o);
$direction = 'r';
}
if($bnode !== null) {
// get bnode name (try canonical, path, then hash)
if($namer->isNamed($bnode)) {
$name = $namer->getName($bnode);
}
else if($path_namer->isNamed($bnode)) {
$name = $path_namer->getName($bnode);
}
else if(property_exists($cache, $bnode)) {
$name = $cache->{$bnode};
}
else {
$name = $this->_hashStatements($bnodes->{$bnode}, $namer);
$cache->{$bnode} = $name;
}
// hash direction, property, and bnode name/hash
$group_md = hash_init('sha1');
hash_update($group_md, $direction);
hash_update($group_md,
($statement->p === '@type') ? self::RDF_TYPE : $statement->p);
hash_update($group_md, $name);
$group_hash = hash_final($group_md);
// add bnode to hash group
if(property_exists($groups, $group_hash)) {
$groups->{$group_hash}[] = $bnode;
}
else {
$groups->{$group_hash} = array($bnode);
}
}
}
// iterate over groups in sorted hash order
$group_hashes = get_object_vars($groups);
sort($group_hashes);
foreach($group_hashes as $group_hash) {
// digest group hash
hash_update($md, $group_hash);
// choose a path and namer from the permutations
$chosen_path = null;
$chosen_namer = null;
$permutator = new Permutator($groups->{$group_hash});
while($permutator->hasNext()) {
$permutation = $permutator->next();
$path_namer_copy = clone $path_namer;
// build adjacent path
$path = '';
$skipped = false;
$recurse = array();
foreach($permutation as $bnode) {
// use canonical name if available
if($namer->isNamed($bnode)) {
$path .= $namer->getName($bnode);
}
else {
// recurse if bnode isn't named in the path yet
if(!$path_namer_copy->isNamed($bnode)) {
$recurse[] = $bnode;
}
$path .= $path_namer_copy->getName($bnode);
}
// skip permutation if path is already >= chosen path
if($chosen_path !== null && strlen($path) >= strlen($chosen_path) &&
$path > $chosen_path) {
$skipped = true;
break;
}
}
// recurse
if($skipped) {
foreach($recurse as $bnode) {
$result = $this->_hashPaths(
$bnodes, $bnodes->{$bnode}, $namer, $path_namer_copy);
$path .= $path_namer_copy->getName($bnode);
$path .= "<{$result->hash}>";
$path_namer_copy = $result->pathNamer;
// skip permutation if path is already >= chosen path
if($chosen_path !== null && strlen($path) >= strlen($chosen_path) &&
$path > $chosenPath) {
$skipped = true;
break;
}
}
}
if(!$skipped && ($chosen_path === null || $path < $chosen_path)) {
$chosen_path = $path;
$chosen_namer = $path_namer_copy;
}
}
// digest chosen path and update namer
hash_update($md, $chosen_path);
$path_namer = $chosen_namer;
}
// return SHA-1 hash and path namer
return (object)array(
'hash' => hash_final($md), 'pathNamer' => $path_namer);
}
/**
* A helper function that gets the blank node name from a statement value
* (a subject or object). If the statement value is not a blank node or it
* has an @id of '_:a', then null will be returned.
*
* @param mixed $value the statement value.
*
* @return mixed the blank node name or null if none was found.
*/
protected function _getBlankNodeName($value) {
return ((self::_isBlankNode($value) && $value->{'@id'} !== '_:a') ?
$value->{'@id'} : null);
}
/**
* Recursively flattens the subjects in the given JSON-LD expanded input.
*
* @param stdClass $subjects a map of subject @id to subject.
* @param mixed $input the JSON-LD expanded input.
* @param UniqueNamer $namer the blank node namer.
* @param mixed $name the name assigned to the current input if it is a bnode.
* @param mixed $list the list to append to, null for none.
*/
protected function _flatten($subjects, $input, $namer, $name, $list) {
// recurse through array
if(is_array($input)) {
foreach($input as $e) {
$this->_flatten($subjects, $e, $namer, null, $list);
}
}
// handle subject
else if(is_object($input)) {
// add value to list
if(self::_isValue($input) && $list) {
$list[] = $input;
return;
}
// get name for subject
if($name === null) {
if(property_exists($input, '@id')) {
$name = $input->{'@id'};
}
if(self::_isBlankNode($input)) {
$name = $namer->getName($name);
}
}
// add subject reference to list
if($list !== null) {
$list[] = (object)array('@id' => $name);
}
// create new subject or merge into existing one
if(property_exists($subjects, $name)) {
$subject = $subjects->{$name};
}
else {
$subject = $subjects->{$name} = new stdClass();
}
$subject->{'@id'} = $name;
foreach($input as $prop => $objects) {
// skip @id
if($prop === '@id') {
continue;
}
// copy keywords
if(self::_isKeyword($prop)) {
$subject->{$prop} = $objects;
continue;
}
// iterate over objects
foreach($objects as $o) {
// handle embedded subject or subject reference
if(self::_isSubject($o) || self::_isSubjectReference($o)) {
// rename blank node @id
$id = property_exists($o, '@id') ? $o->{'@id'} : '_:';
if(strpos($id, '_:') === 0) {
$id = $namer->getName($id);
}
// add reference and recurse
self::addValue($subject, $prop, (object)array('@id' => id), true);
$this->_flatten($subjects, $o, $namer, $id, null);
}
else {
// recurse into list
if(self::_isListValue($o)) {
$l = new ArrayObject();
$this->_flatten($subjects, $o->{'@list'}, $namer, $name, $l);
$o = (object)array('@list' => (array)$l);
}
// add non-subject
self::addValue($subject, $prop, $o, true);
}
}
}
}
// add non-object to list
else if($list !== null) {
$list[] = $input;
}
}
/**
* Frames subjects according to the given frame.
*
* @param stdClass $state the current framing state.
* @param array $subjects the subjects to filter.
* @param array $frame the frame.
* @param mixed $parent the parent subject or top-level array.
* @param mixed $property the parent property, initialized to null.
*/
protected function _match_frame(
$state, $subjects, $frame, $parent, $property) {
// validate the frame
$this->_validateFrame($state, $frame);
$frame = $frame[0];
// filter out subjects that match the frame
$matches = $this->_filterSubjects($state, $subjects, $frame);
// get flags for current frame
$options = $state->options;
$embedOn = $this->_getFrameFlag($frame, $options, 'embed');
$explicitOn = $this->_getFrameFlag($frame, $options, 'explicit');
// add matches to output
foreach($matches as $id => $subject) {
/* Note: In order to treat each top-level match as a compartmentalized
result, create an independent copy of the embedded subjects map when the
property is null, which only occurs at the top-level. */
if($property === null) {
$state->embeds = new stdClass();
}
// start output
$output = new stdClass();
$output->{'@id'} = $id;
// prepare embed meta info
$embed = (object)array('parent' => $parent, 'property' => $property);
// if embed is on and there is an existing embed
if($embedOn && property_exists($state->embeds, $id)) {
// only overwrite an existing embed if it has already been added to its
// parent -- otherwise its parent is somewhere up the tree from this
// embed and the embed would occur twice once the tree is added
$embedOn = false;
// existing embed's parent is an array
$existing = $state->embeds->{$id};
if(is_array($existing->parent)) {
foreach($existing->parent as $p) {
if(self::compareValues($output, $p)) {
$embedOn = true;
break;
}
}
}
// existing embed's parent is an object
else if(self::hasValue(
$existing->parent, $existing->property, $output)) {
$embedOn = true;
}
// existing embed has already been added, so allow an overwrite
if($embedOn) {
$this->_removeEmbed($state, $id);
}
}
// not embedding, add output without any other properties
if(!$embedOn) {
$this->_addFrameOutput($state, $parent, $property, $output);
}
else {
// add embed meta info
$state->embeds->{$id} = $embed;
// iterate over subject properties
$props = get_object_vars($subject);
sort($props);
foreach($props as $prop) {
// copy keywords to output
if(self::_isKeyword($prop)) {
$output->{$prop} = self::copy($subject->{$prop});
continue;
}
// if property isn't in the frame
if(!property_exists($frame, $prop)) {
// if explicit is off, embed values
if(!$explicitOn) {
$this->_embedValues($state, $subject, $prop, $output);
}
continue;
}
// add objects
$objects = $subject->{$prop};
foreach($objects as $o) {
// recurse into list
if(self::_isListValue($o)) {
// add empty list
$list = (object)array('@list' => array());
$this->_addFrameOutput($state, $output, $prop, $list);
// add list objects
$src = $o->{'@list'};
foreach($src as $o) {
// recurse into subject reference
if(self::_isSubjectReference($o)) {
$this->_match_frame(
$state, array($o->{'@id'}), $frame->{$prop},
$list, '@list');
}
// include other values automatically
else {
$this->_addFrameOutput(
$state, $list, '@list', self::copy($o));
}
}
continue;
}
// recurse into subject reference
if(self::_isSubjectReference($o)) {
$this->_match_frame(
$state, array($o->{'@id'}), $frame->{$prop}, $output, $prop);
}
// include other values automatically
else {
$this->_addFrameOutput($state, $output, $prop, self::copy($o));
}
}
}
// handle defaults
$props = get_object_vars($frame);
sort($props);
foreach($props as $prop) {
// skip keywords
if(self::_isKeyword($prop)) {
continue;
}
// if omit default is off, then include default values for properties
// that appear in the next frame but are not in the matching subject
$next = $frame->{$prop}[0];
$omitDefaultOn = $this->_getFrameFlag($next, $options, 'omitDefault');
if(!$omitDefaultOn && !property_exists($output, $prop)) {
$preserve = '@null';
if(property_exists($next, '@default')) {
$preserve = self::copy($next->{'@default'});
}
$output->{$prop} = (object)array('@preserve' => $preserve);
}
}
// add output to parent
$this->_addFrameOutput($state, $parent, $property, $output);
}
}
}
/**
* Gets the frame flag value for the given flag name.
*
* @param stdClass $frame the frame.
* @param stdClass $options the framing options.
* @param string $name the flag name.
*
* @return mixed $the flag value.
*/
protected function _getFrameFlag($frame, $options, $name) {
$flag = "'@'$name";
return (property_exists($frame, $flag) ?
$frame->{$flag}[0] : $options->{$name});
}
/**
* Validates a JSON-LD frame, throwing an exception if the frame is invalid.
*
* @param stdClass $state the current frame state.
* @param array $frame the frame to validate.
*/
protected function _validateFrame($state, $frame) {
if(!is_array($frame) || count($frame) !== 1 || !is_object($frame[0])) {
throw new JsonLdException(
'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.',
'jsonld.SyntaxError', array('frame' => $frame));
}
}
/**
* Returns a map of all of the subjects that match a parsed frame.
*
* @param stdClass $state the current framing state.
* @param array $subjects the set of subjects to filter.
* @param stdClass $frame the parsed frame.
*
* @return stdClass all of the matched subjects.
*/
protected function _filterSubjects($state, $subjects, $frame) {
$rval = new stdClass();
sort($subjects);
foreach($subjects as $id) {
$subject = $state->subjects->{$id};
if($this->_filterSubject($subject, $frame)) {
$rval->{$id} = $subject;
}
}
return $rval;
}
/**
* Returns true if the given subject matches the given frame.
*
* @param stdClass $subject the subject to check.
* @param stdClass $frame the frame to check.
*
* @return bool true if the subject matches, false if not.
*/
protected function _filterSubject($subject, $frame) {
// check @type (object value means 'any' type, fall through to ducktyping)
if(property_exists($frame, '@type') &&
!(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) {
$types = $frame->{'@type'};
foreach($types as $type) {
// any matching @type is a match
if(self::hasValue($subject, '@type', $type)) {
return true;
}
}
return false;
}
// check ducktype
foreach($frame as $key) {
// only not a duck if @id or non-keyword isn't in subject
if(($key === '@id' || !self::_isKeyword($key)) &&
!property_exists($subject, $key)) {
return false;
}
}
return true;
}
/**
* Embeds values for the given subject and property into the given output
* during the framing algorithm.
*
* @param stdClass $state the current framing state.
* @param stdClass $subject the subject.
* @param string $property the property.
* @param mixed $output the output.
*/
protected function _embedValues($state, $subject, $property, $output) {
// embed subject properties in output
$objects = $subject->{$property};
foreach($objects as $o) {
// recurse into @list
if(self::_isListValue($o)) {
$list = (object)array('@list' => new ArrayObject());
$this->_addFrameOutput($state, $output, $property, $list);
$this->_embedValues($state, $o, '@list', $list->{'@list'});
$list->{'@list'} = (array)$list->{'@list'};
return;
}
// handle subject reference
if(self::_isSubjectReference($o)) {
$id = $o->{'@id'};
// embed full subject if isn't already embedded
if(!property_exists($state->embeds, $id)) {
// add embed
$embed = (object)array('parent' => $output, 'property' => $property);
$state->embeds->{$id} = $embed;
// recurse into subject
$o = new stdClass();
$s = $state->subjects->{$id};
foreach($s as $prop => $v) {
// copy keywords
if(self::_isKeyword($prop)) {
$o->{$prop} = self::copy($v);
continue;
}
$this->_embedValues($state, $s, $prop, $o);
}
}
$this->_addFrameOutput($state, $output, $property, $o);
}
// copy non-subject value
else {
$this->_addFrameOutput($state, $output, $property, self::copy($o));
}
}
}
/**
* Removes an existing embed.
*
* @param stdClass $state the current framing state.
* @param string $id the @id of the embed to remove.
*/
protected function _removeEmbed($state, $id) {
// get existing embed
$embeds = $state->embeds;
$embed = $embeds->{$id};
$property = $embed->property;
// create reference to replace embed
$subject = (object)array('@id' => id);
// remove existing embed
if(is_array($embed->parent)) {
// replace subject with reference
foreach($embed->parent as $i => $parent) {
if(self::compareValues($parent, $subject)) {
$embed->parent[$i] = $subject;
break;
}
}
}
else {
// replace subject with reference
$useArray = is_array($embed->parent->{$property});
self::removeValue($embed->parent, $property, $subject, $useArray);
self::addValue($embed->parent, $property, $subject, $useArray);
}
// recursively remove dependent dangling embeds
$removeDependents = function($id) {
// get embed keys as a separate array to enable deleting keys in map
$ids = get_object_vars($embeds);
foreach($ids as $next) {
if(property_exists($embeds, $next) &&
is_object($embeds->{$next}->parent) &&
$embeds->{$next}->parent->{'@id'} === $id) {
unset($embeds->{$next});
$removeDependents($next);
}
}
};
$removeDependents($id);
}
/**
* Adds framing output to the given parent.
*
* @param stdClass $state the current framing state.
* @param mixed $parent the parent to add to.
* @param string $property the parent property.
* @param mixed $output the output to add.
*/
protected function _addFrameOutput($state, $parent, $property, $output) {
if(is_object($parent)) {
self::addValue($parent, $property, $output, true);
}
else {
$parent[] = $output;
}
}
/**
* Removes the @preserve keywords as the last step of the framing algorithm.
*
* @param stdClass $ctx the active context used to compact the input.
* @param mixed $input the framed, compacted output.
*
* @return mixed the resulting output.
*/
protected function _removePreserve($ctx, $input) {
// recurse through arrays
if(is_array($input)) {
$output = array();
foreach($input as $e) {
$result = $this->_removePreserve($ctx, $e);
// drop nulls from arrays
if($result !== null) {
$output[] = $result;
}
}
$input = $output;
}
else if(is_object($input)) {
// remove @preserve
if(property_exists($input, '@preserve')) {
if($input->{'@preserve'} === '@null') {
return null;
}
return $input->{'@preserve'};
}
// skip @values
if(self::_isValue($input)) {
return $input;
}
// recurse through @lists
if(self::_isListValue($input)) {
$input->{'@list'} = $this->_removePreserve($ctx, $input->{'@list'});
return $input;
}
// recurse through properties
foreach($input as $prop => $v) {
$result = $this->_removePreserve($ctx, $v);
$container = self::getContextValue($ctx, $prop, '@container');
if(is_array($result) && count($result) === 1 &&
$container !== '@set' && $container !== '@list') {
$result = $result[0];
}
$input->{$prop} = $result;
}
}
return $input;
}
/**
* Compares two strings first based on length and then lexicographically.
*
* @param string $a the first string.
* @param string $b the second string.
*
* @return integer -1 if a < b, 1 if a > b, 0 if a == b.
*/
protected function _compareShortestLeast($a, $b) {
if(strlen($a) < strlen($b)) {
return -1;
}
else if(strlen($b) < strlen($a)) {
return 1;
}
return ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
}
/**
* Ranks a term that is possible choice for compacting an IRI associated with
* the given value.
*
* @param stdClass $ctx the active context.
* @param string $term the term to rank.
* @param mixed $value the associated value.
*
* @return integer the term rank.
*/
protected function _rankTerm($ctx, $term, $value) {
// no term restrictions for a null value
if($value === null) {
return 3;
}
// get context entry for term
$entry = $ctx->mappings->{$term};
$hasType = property_exists($entry, '@type');
$hasLanguage = property_exists($entry, '@language');
$hasDefaultLanguage = property_exists($ctx, '@language');
// @list rank is the sum of its values' ranks
if(self::_isListValue($value)) {
$list = $value->{'@list'};
if(count($list) === 0) {
return ($entry->{'@container'} === '@list') ? 1 : 0;
}
// sum term ranks for each list value
$sum = 0;
foreach($list as $v) {
$sum += $this->_rankTerm($ctx, $term, $v);
}
return $sum;
}
// rank boolean or number
if(is_bool($value) || is_numeric($value)) {
if(is_bool($value)) {
$type = self::XSD_BOOLEAN;
}
else if(is_double(value)) {
$type = self::XSD_DOUBLE;
}
else {
$type = self::XSD_INTEGER;
}
if($entry->{'@type'} === $type) {
return 3;
}
return (!$hasType && !$hasLanguage) ? 2 : 1;
}
// rank string (this means the value has no @language)
if(is_string($value)) {
// entry @language is specifically null or no @type, @language, or default
if(($hasLanguage && $entry->{'@language'} === null) ||
(!$hasType && !$hasLanguage && !$hasDefaultLanguage)) {
return 3;
}
return 0;
}
// Note: Value must be an object that is a @value or subject/reference.
// @value must have either @type or @language
if(self::_isValue($value)) {
if(property_exists($value, '@type')) {
// @types match
if($hasType && $value->{'@type'} === $entry->{'@type'}) {
return 3;
}
return (!$hasType && !$hasLanguage) ? 1 : 0;
}
// @languages match or entry has no @type or @language but default
// @language matches
if(($hasLanguage && $value->{'@language'} === $entry->{'@language'}) ||
(!$hasType && !$hasLanguage &&
$value->{'@language'} === $ctx->{'@language'})) {
return 3;
}
return (!$hasType && !$hasLanguage) ? 1 : 0;
}
// value must be a subject/reference
if($entry->{'@type'} === '@id') {
return 3;
}
return (!$hasType && !$hasLanguage) ? 1 : 0;
}
/**
* Compacts an IRI or keyword into a term or prefix if it can be. If the
* IRI has an associated value it may be passed.
*
* @param stdClass $ctx the active context to use.
* @param string $iri the IRI to compact.
* @param mixed $value the value to check or null.
*
* @return string the compacted term, prefix, keyword alias, or original IRI.
*/
protected function _compactIri($ctx, $iri, $value=null) {
// can't compact null
if($iri === null) {
return $iri;
}
// compact rdf:type
if($iri === self::RDF_TYPE) {
return '@type';
}
// term is a keyword
if(self::_isKeyword($iri)) {
// return alias if available
$aliases = $ctx->keywords->{$iri};
if(count($aliases) > 0) {
return $aliases[0];
}
else {
// no alias, keep original keyword
return $iri;
}
}
// find all possible term matches
$terms = array();
$highest = 0;
$listContainer = false;
$isList = self::_isListValue($value);
foreach($ctx->mappings as $term) {
// skip terms with non-matching iris
$entry = $ctx->mappings->{$term};
if($entry->{'@id'} !== $iri) {
continue;
}
// skip @set containers for @lists
if($isList && $entry->{'@container'} === '@set') {
continue;
}
// skip @list containers for non-@lists
if(!$isList && $entry->{'@container'} === '@list') {
continue;
}
// for @lists, if listContainer is set, skip non-list containers
if($isList && $listContainer && $entry->{'@container'} !== '@list') {
continue;
}
// rank term
$rank = $this->_rankTerm($ctx, $term, $value);
if($rank > 0) {
// add 1 to rank if container is a @set
if($entry->{'@container'} === '@set') {
$rank += 1;
}
// for @lists, give preference to @list containers
if($isList && !$listContainer && $entry->{'@container'} === '@list') {
$listContainer = true;
$terms = array();
$highest = $rank;
$terms[] = $term;
}
// only push match if rank meets current threshold
else if($rank >= $highest) {
if($rank > $highest) {
$terms = array();
$highest = $rank;
}
$terms[] = $term;
}
}
}
// no term matches, add possible CURIEs
if(count($terms) === 0) {
foreach($ctx->mappings as $term => $entry) {
// skip terms with colons, they can't be prefixes
if(strpos($term, ':') !== false) {
continue;
}
// skip entries with @ids that are not partial matches
if($entry->{'@id'} === $iri || strpos($iri, $entry->{'@id'}) !== 0) {
continue;
}
// add CURIE as term if it has no mapping
$curie = $term . ':' . substr($iri, strlen($entry->{'@id'}));
if(!property_exists($ctx->mappings, $curie)) {
$terms[] = $curie;
}
}
}
// no matching terms, use IRI
if(count($terms) === 0) {
return $iri;
}
// return shortest and lexicographically-least term
usort($terms, array($this, '_compareShortestLeast'));
return $terms[0];
}
/**
* Defines a context mapping during context processing.
*
* @param stdClass $active_ctx the current active context.
* @param stdClass $ctx the local context being processed.
* @param string $key the key in the local context to define the mapping for.
* @param string $base the base IRI.
* @param stdClass $defined a map of defining/defined keys to detect cycles
* and prevent double definitions.
*/
protected function _defineContextMapping(
$active_ctx, $ctx, $key, $base, $defined) {
if(property_exists($defined, $key)) {
// key already defined
if($defined->{$key}) {
return;
}
// cycle detected
throw new JsonLdException(
'Cyclical context definition detected.',
'jsonld.CyclicalContext',
(object)array('context' => $ctx, 'key' => $key));
}
// now defining key
$defined->{$key} = false;
// if key has a prefix, define it first
$colon = strpos($key, ':');
$prefix = null;
if($colon !== false) {
$prefix = substr($key, 0, $colon);
if(property_exists($ctx, $prefix)) {
// define parent prefix
$this->_defineContextMapping(
$active_ctx, $ctx, $prefix, $base, $defined);
}
}
// get context key value
$value = $ctx->{$key};
if(self::_isKeyword($key)) {
// only @language is permitted
if($key !== '@language') {
throw new JsonLdException(
'Invalid JSON-LD syntax; keywords cannot be overridden.',
'jsonld.SyntaxError', array('context' => $ctx));
}
if($value !== null && !is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; the value of "@language" in a ' +
'@context must be a string or null.',
'jsonld.SyntaxError', array('context' => $ctx));
}
if($value === null) {
unset($active_ctx->{'@language'});
}
else {
$active_ctx->{'@language'} = $value;
}
$defined->{$key} = true;
return;
}
// clear context entry
if($value === null) {
if(property_exists($active_ctx->mappings, $key)) {
// if key is a keyword alias, remove it
$kw = $active_ctx->mappings->{$key}->{'@id'};
if(self::_isKeyword($kw)) {
array_splice($active_ctx->keywords->{$kw},
in_array($key, $active_ctx->keywords->{$kw}), 1);
}
unset($active_ctx->mappings->{$key});
}
$defined->{$key} = true;
return;
}
if(is_string($value)) {
if(self::_isKeyword($value)) {
// disallow aliasing @context and @preserve
if($value === '@context' || $value === '@preserve') {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.',
'jsonld.SyntaxError');
}
// uniquely add key as a keyword alias and resort
$aliases = $active_ctx->keywords->{$value};
if(in_array($key, $active_ctx->keywords->{$value}) === false) {
$active_ctx->keywords->{$value}[] = $key;
usort($active_ctx->keywords->{$value},
array($this, '_compareShortestLeast'));
}
}
else {
// expand value to a full IRI
$value = $this->_expandContextIri(
$active_ctx, $ctx, $value, $base, $defined);
}
// define/redefine key to expanded IRI/keyword
$active_ctx->mappings->{$key} = (object)array('@id' => $value);
$defined->{$key} = true;
return;
}
if(!is_object($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context property values must be ' +
'strings or objects.',
'jsonld.SyntaxError', array('context' => $ctx));
}
// create new mapping
$mapping = new stdClass();
if(property_exists($value, '@id')) {
$id = $value->{'@id'};
if(!is_string($id)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @id values must be strings.',
'jsonld.SyntaxError', array('context' => $ctx));
}
// expand @id to full IRI
$id = $this->_expandContextIri($active_ctx, $ctx, $id, $base, $defined);
// add @id to mapping
$mapping->{'@id'} = $id;
}
else {
// non-IRIs *must* define @ids
if($prefix === null) {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context terms must define an @id.',
'jsonld.SyntaxError', array('context' => $ctx, 'key' => $key));
}
// set @id based on prefix parent
if(property_exists($active_ctx->mappings, $prefix)) {
$suffix = substr($key, $colon + 1);
$mapping->{'@id'} = $active_ctx->mappings->{$prefix}->{'@id'} . $suffix;
}
// key is an absolute IRI
else {
$mapping->{'@id'} = $key;
}
}
if(property_exists($value, '@type')) {
$type = $value->{'@type'};
if(!is_string($type)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @type values must be strings.',
'jsonld.SyntaxError', array('context' => $ctx));
}
if($type !== '@id') {
// expand @type to full IRI
$type = $this->_expandContextIri(
$active_ctx, $ctx, $type, '', $defined);
}
// add @type to mapping
$mapping->{'@type'} = $type;
}
if(property_exists($value, '@container')) {
$container = $value->{'@container'};
if($container !== '@list' && $container !== '@set') {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @container value must be ' +
'"@list" or "@set".',
'jsonld.SyntaxError', array('context' => $ctx));
}
// add @container to mapping
$mapping->{'@container'} = $container;
}
if(property_exists($value, '@language') &&
!property_exists($value, '@type')) {
$language = $value->{'@language'};
if($language !== null && !is_string($language)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; @context @language value must be ' +
'a string or null.',
'jsonld.SyntaxError', array('context' => $ctx));
}
// add @language to mapping
$mapping->{'@language'} = $language;
}
// merge onto parent mapping if one exists for a prefix
if($prefix !== null && property_exists($active_ctx->mappings, $prefix)) {
$child = $mapping;
$mapping = self::copy($active_ctx->mappings->{$prefix});
foreach($child as $k => $v) {
$mapping->{$k} = $v;
}
}
// define key mapping
$active_ctx->mappings->{$key} = $mapping;
$defined->{$key} = true;
}
/**
* Expands a string value to a full IRI during context processing. It can
* be assumed that the value is not a keyword.
*
* @param stdClass $active_ctx the current active context.
* @param stdClass $ctx the local context being processed.
* @param string $value the string value to expand.
* @param string $base the base IRI.
* @param stdClass $defined a map for tracking cycles in context definitions.
*
* @return mixed the expanded value.
*/
protected function _expandContextIri(
$active_ctx, $ctx, $value, $base, $defined) {
// dependency not defined, define it
if(property_exists($ctx, $value) && $defined->{$value} !== true) {
$this->_defineContextMapping($active_ctx, $ctx, $value, $base, $defined);
}
// recurse if value is a term
if(property_exists($active_ctx->mappings, $value)) {
$id = $active_ctx->mappings->{$value}->{'@id'};
// value is already an absolute IRI
if($value === $id) {
return $value;
}
return _expandContextIri($active_ctx, $ctx, $id, $base, $defined);
}
// split value into prefix:suffix
if(strpos($value, ':') !== false) {
list($prefix, $suffix) = explode(':', $value, 2);
// a prefix of '_' indicates a blank node
if($prefix === '_') {
return $value;
}
// a suffix of '//' indicates value is an absolute IRI
if(strpos($suffix, '//') === 0) {
return $value;
}
// dependency not defined, define it
if(property_exists($ctx, $prefix) && $defined->{$prefix} !== true) {
$this->_defineContextMapping(
$active_ctx, $ctx, $prefix, $base, $defined);
}
// recurse if prefix is defined
if(property_exists($active_ctx->mappings, $prefix)) {
$id = $active_ctx->mappings->{$prefix}->{'@id'};
return $this->_expandContextIri(
$active_ctx, $ctx, $id, $base, $defined) . $suffix;
}
// consider value an absolute IRI
return $value;
}
// prepend base
$value = "$base$value";
// value must now be an absolute IRI
if(!self::_isAbsoluteIri($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; a @context value does not expand to ' +
'an absolute IRI.',
'jsonld.SyntaxError', array('context' => $ctx, 'value' => $value));
}
return $value;
}
/**
* Expands a term into an absolute IRI. The term may be a regular term, a
* prefix, a relative IRI, or an absolute IRI. In any case, the associated
* absolute IRI will be returned.
*
* @param stdClass $ctx the active context to use.
* @param string $term the term to expand.
* @param string $base the base IRI to use if a relative IRI is detected.
*
* @return string the expanded term as an absolute IRI.
*/
protected function _expandTerm($ctx, $term, $base='') {
// nothing to expand
if($term === null) {
return null;
}
// the term has a mapping, so it is a plain term
if(property_exists($ctx->mappings, $term)) {
$id = $ctx->mappings->{$term}->{'@id'};
// term is already an absolute IRI
if($term === $id) {
return $term;
}
return $this->_expandTerm($ctx, $id, $base);
}
// split term into prefix:suffix
if(strpos($term, ':') !== false) {
list($prefix, $suffix) = explode(':', $term, 2);
// a prefix of '_' indicates a blank node
if($prefix === '_') {
return $term;
}
// a suffix of '//' indicates value is an absolute IRI
if(strpos($suffix, '//') === 0) {
return $term;
}
// the term's prefix has a mapping, so it is a CURIE
if(property_exists($ctx->mappings, $prefix)) {
return $this->_expandTerm(
$ctx, $ctx->mappings->{$prefix}->{'@id'}, $base) . $suffix;
}
// consider term an absolute IRI
return $term;
}
// prepend base to term
return "$base$term";
}
/**
* Resolves external @context URLs using the given URL resolver. Each instance
* of @context in the input that refers to a URL will be replaced with the
* JSON @context found at that URL.
*
* @param mixed $input the JSON-LD object with possible contexts.
* @param callable $resolver(url, callback(err, jsonCtx)) the URL resolver.
*
* @return mixed the result.
*/
protected function _resolveUrls($input, $resolver) {
// keeps track of resolved URLs (prevents duplicate work)
$urls = new stdClass();
// finds URLs in @context properties and replaces them with their
// resolved @contexts if replace is true
$findUrls = function($input, $replace) use (&$findUrls, $urls) {
if(is_array($input)) {
$output = array();
foreach($input as $v) {
$output[] = $findUrls($v, $replace);
}
return $output;
}
else if(is_object($input)) {
foreach($input as $k => $v) {
if($k !== '@context') {
$input->{$k} = $findUrls($v, $replace);
continue;
}
// array @context
if(is_array($v)) {
foreach($v as $i => $url) {
if(is_string($url)) {
// replace w/resolved @context if requested
if($replace) {
$v[$i] = $urls->{$url};
}
// unresolved @context found
else if(!property_exists($urls, $url)) {
$urls->{$url} = new stdClass();
}
}
}
}
// string @context
else if(is_string($v)) {
// replace w/resolved @context if requested
if($replace) {
$input->{$key} = $urls->{$v};
}
// unresolved @context found
else if(!property_exists($urls, $v)) {
$urls->{$v} = new stdClass();
}
}
}
}
return $input;
};
$input = $findUrls($input, false);
// resolve all URLs
foreach($urls as $url => $v) {
// validate URL
if(filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new JsonLdException(
'Malformed URL.', 'jsonld.InvalidUrl', array('url' => $url));
}
// resolve URL
$ctx = $resolver($url);
// parse string context as JSON
if(is_string($ctx)) {
$ctx = json_decode($ctx);
switch(json_last_error()) {
case JSON_ERROR_NONE:
break;
case JSON_ERROR_DEPTH:
throw new JsonLdException(
'Could not parse JSON from URL; the maximum stack depth has ' .
'been exceeded.', 'jsonld.ParseError', array('url' => $url));
case JSON_ERROR_STATE_MISMATCH:
throw new JsonLdException(
'Could not parse JSON from URL; invalid or malformed JSON.',
'jsonld.ParseError', array('url' => $url));
case JSON_ERROR_CTRL_CHAR:
case JSON_ERROR_SYNTAX:
throw new JsonLdException(
'Could not parse JSON from URL; syntax error, malformed JSON.',
'jsonld.ParseError', array('url' => $url));
case JSON_ERROR_UTF8:
throw new JsonLdException(
'Could not parse JSON from URL; malformed UTF-8 characters.',
'jsonld.ParseError', array('url' => $url));
default:
throw new JsonLdException(
'Could not parse JSON from URL; unknown error.',
'jsonld.ParseError', array('url' => $url));
}
}
// ensure ctx is an object
if(!is_object($ctx)) {
throw new JsonLdException(
'URL does not resolve to a valid JSON-LD object.',
'jsonld.InvalidUrl', array('url' => $url));
}
// FIXME: needs to recurse to resolve URLs in the result, and
// detect cycles, and limit recursion
if(property_exists($ctx, '@context')) {
$urls->{$url} = $ctx->{'@context'};
}
}
// do url replacement
return $findUrls($input, true);
}
/**
* Gets the initial context.
*
* @return stdClass the initial context.
*/
protected function _getInitialContext() {
return (object)array(
'mappings' => new stdClass(),
'keywords' => (object)array(
'@context'=> array(),
'@container'=> array(),
'@default'=> array(),
'@embed'=> array(),
'@explicit'=> array(),
'@graph'=> array(),
'@id'=> array(),
'@language'=> array(),
'@list'=> array(),
'@omitDefault'=> array(),
'@preserve'=> array(),
'@set'=> array(),
'@type'=> array(),
'@value'=> array()
));
}
/**
* Returns whether or not the given value is a keyword (or a keyword alias).
*
* @param string $value the value to check.
* @param stdClass [$ctx] the active context to check against.
*
* @return bool true if the value is a keyword, false if not.
*/
protected static function _isKeyword($value, $ctx=null) {
if($ctx !== null) {
if(property_exists($ctx->keywords, $value)) {
return true;
}
foreach($ctx->keywords as $kw => $aliases) {
if(in_array($value, $aliases) !== false) {
return true;
}
}
}
else {
switch($value) {
case '@context':
case '@container':
case '@default':
case '@embed':
case '@explicit':
case '@graph':
case '@id':
case '@language':
case '@list':
case '@omitDefault':
case '@preserve':
case '@set':
case '@type':
case '@value':
return true;
}
}
return false;
}
/**
* Returns true if the given input is an empty Object.
*
* @param mixed $input the input to check.
*
* @return bool true if the input is an empty Object, false if not.
*/
protected static function _isEmptyObject($input) {
return is_object($input) && count(get_object_vars($input)) === 0;
}
/**
* Returns true if the given input is an Array of Strings.
*
* @param mixed $input the input to check.
*
* @return bool true if the input is an Array of Strings, false if not.
*/
protected static function _isArrayOfStrings($input) {
if(!is_array($input)) {
return false;
}
foreach($input as $v) {
if(!is_string($v)) {
return false;
}
}
return true;
}
/**
* Returns true if the given value is a subject with properties.
*
* @param mixed $value the value to check.
*
* @return bool true if the value is a subject with properties, false if not.
*/
protected static function _isSubject($value) {
$rval = false;
// Note: A value is a subject if all of these hold true:
// 1. It is an Object.
// 2. It is not a @value, @set, or @list.
// 3. It has more than 1 key OR any existing key is not @id.
if(is_object($value) &&
!property_exists($value, '@value') &&
!property_exists($value, '@set') &&
!property_exists($value, '@list')) {
$count = count(get_object_vars($value));
$rval = ($count > 1 || !property_exists($value, '@id'));
}
return $rval;
}
/**
* Returns true if the given value is a subject reference.
*
* @param mixed $value the value to check.
*
* @return bool true if the value is a subject reference, false if not.
*/
protected static function _isSubjectReference($value) {
// Note: A value is a subject reference if all of these hold true:
// 1. It is an Object.
// 2. It has a single key: @id.
return (is_object($value) && count(get_object_vars($value)) === 1 &&
property_exists($value, '@id'));
}
/**
* Returns true if the given value is a @value.
*
* @param mixed $value the value to check.
*
* @return bool true if the value is a @value, false if not.
*/
protected static function _isValue($value) {
// Note: A value is a @value if all of these hold true:
// 1. It is an Object.
// 2. It has the @value property.
return is_object($value) && property_exists($value, '@value');
}
/**
* Returns true if the given value is a @set.
*
* @param mixed $value the value to check.
*
* @return bool true if the value is a @set, false if not.
*/
protected static function _isSetValue($value) {
// Note: A value is a @set if all of these hold true:
// 1. It is an Object.
// 2. It has the @set property.
return is_object($value) && property_exists($value, '@set');
}
/**
* Returns true if the given value is a @list.
*
* @param mixed $value the value to check.
*
* @return bool true if the value is a @list, false if not.
*/
protected static function _isListValue($value) {
// Note: A value is a @list if all of these hold true:
// 1. It is an Object.
// 2. It has the @list property.
return is_object($value) && property_exists($value, '@list');
}
/**
* Returns true if the given value is a blank node.
*
* @param mixed $value the value to check.
*
* @return bool true if the value is a blank node, false if not.
*/
protected static function _isBlankNode($value) {
$rval = false;
// Note: A value is a blank node if all of these hold true:
// 1. It is an Object.
// 2. If it has an @id key its value begins with '_:'.
// 3. It has no keys OR is not a @value, @set, or @list.
if(is_object($value)) {
if(property_exists($value, '@id')) {
$rval = (strpos($value->{'@id'}, '_:') === 0);
}
else {
$rval = (count(get_object_vars($value)) === 0 ||
!(property_exists($value, '@value') ||
property_exists($value, '@set') ||
property_exists($value, '@list')));
}
}
return $rval;
}
/**
* Returns true if the given value is an absolute IRI, false if not.
*
* @param string $value the value to check.
*
* @return bool true if the value is an absolute IRI, false if not.
*/
protected static function _isAbsoluteIri($value) {
return strpos($value, ':') !== false;
}
}
?>
/**
* A JSON-LD Exception.
*/
class JsonLdException extends Exception {
protected $type;
protected $details;
protected $cause;
public function __construct($msg, $type, $details=null, $previous=null) {
$this->type = $type;
$this->details = $details;
$this->cause = $previous;
parent::__construct($msg, 0, $previous);
}
public function __toString() {
$rval = __CLASS__ . ": [{$this->type}]: {$this->message}\n";
if($this->details) {
$rval .= 'details: ' . print_r($this->details, true) . "\n";
}
if($this->cause) {
$rval .= 'cause: ' . $this->cause->toString();
}
$rval .= $this->getTraceAsString() . "\n";
return $rval;
}
};
/**
* A UniqueNamer issues unique names, keeping track of any previously issued
* names.
*/
class UniqueNamer {
protected $prefix;
protected $counter;
protected $existing;
protected $order;
/**
* Constructs a new UniqueNamer.
*
* @param prefix the prefix to use ('<prefix><counter>').
*/
public function __construct($prefix) {
$this->prefix = $prefix;
$this->counter = 0;
$this->existing = new stdClass();
$this->order = array();
}
/**
* Clones this UniqueNamer.
*/
public function __clone() {
$this->existing = clone $this->existing;
}
/**
* Gets the new name for the given old name, where if no old name is given
* a new name will be generated.
*
* @param mixed [$old_name] the old name to get the new name for.
*
* @return string the new name.
*/
public function getName($old_name=null) {
// return existing old name
if($old_name && property_exists($this->existing, $old_name)) {
return $this->existing->{$oldName};
}
// get next name
$name = $this->prefix . $this->counter;
$this->counter += 1;
// save mapping
if($old_name !== null) {
$this->existing->{$oldName} = $name;
}
return $name;
}
/**
* Returns true if the given old name has already been assigned a new name.
*
* @param string $old_name the old name to check.
*
* @return true if the oldName has been assigned a new name, false if not.
*/
public function isNamed($old_name) {
return property_exists($this->existing, $old_name);
}
}
/**
* A Permutator iterates over all possible permutations of the given array
* of elements.
*/
class Permutator {
protected $list;
protected $done;
protected $left;
/**
* Constructs a new Permutator.
*
* @param array $list the array of elements to iterate over.
*/
public function __constructor($list) {
// original array
$this->list = $list;
sort($this->list);
// indicates whether there are more permutations
$this->done = false;
// directional info for permutation algorithm
$this->left = new stdClass();
foreach($list as $v) {
$this->left->{$v} = true;
}
}
/**
* Returns true if there is another permutation.
*
* @return bool true if there is another permutation, false if not.
*/
public function hasNext() {
return !$this->done;
}
/**
* Gets the next permutation. Call hasNext() to ensure there is another one
* first.
*
* @return array the next permutation.
*/
public function next() {
// copy current permutation
$rval = $this->list;
/* Calculate the next permutation using the Steinhaus-Johnson-Trotter
permutation algorithm. */
// get largest mobile element k
// (mobile: element is greater than the one it is looking at)
$k = null;
$pos = 0;
$length = count($this->list);
for($i = 0; $i < $length; ++$i) {
$element = $this->list[$i];
$left = $this->left->{$element};
if(($k === null || $element > $k) &&
(($left && $i > 0 && $element > $this->list[$i - 1]) ||
(!$left && $i < ($length - 1) && $element > $this->list[$i + 1]))) {
$k = $element;
$pos = $i;
}
}
// no more permutations
if($k === null) {
$this->done = true;
}
else {
// swap k and the element it is looking at
$swap = $this->left->{$k} ? $pos - 1 : $pos + 1;
$this->list[$pos] = $this->list[$swap];
$this->list[$swap] = $k;
// reverse the direction of all elements larger than k
for($i = 0; $i < $length; ++$i) {
if($this->list[$i] > $k) {
$this->left->{$this->list[$i]} = !$this->left->{$this->list[$i]};
}
}
}
return $rval;
}
}
/* end of file, omit ?> */