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