diff --git a/jsonld-tests.php b/jsonld-tests.php
index 797fbe2..878ee4f 100644
--- a/jsonld-tests.php
+++ b/jsonld-tests.php
@@ -12,313 +12,275 @@ require_once('jsonld.php');
$isCli = defined('STDIN');
$eol = $isCli ? "\n" : '
';
-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: ,
+ sequence: [{
+ 'name': ,
+ '@type': ["test:TestCase", "jld:"],
+ 'input': ,
+ 'context': ,
+ 'frame': ,
+ 'expect': ,
+ }]
+ }
+ */
+ 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: ,
- sequence: [{
- 'name': ,
- '@type': ["test:TestCase", "jld:"],
- 'input': ,
- 'context': ,
- 'frame': ,
- 'expect': ,
- }]
- }
- */
- 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";
diff --git a/jsonld.php b/jsonld.php
index d610e8e..e64abf2 100644
--- a/jsonld.php
+++ b/jsonld.php
@@ -1,2933 +1,3402 @@
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 ('').
+ */
+ 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 ?> */