php-json-ld/jsonld.php

2934 lines
77 KiB
PHP

<?php
/**
* PHP implementation of JSON-LD.
*
* @author Dave Longley
*
* Copyright (c) 2011-2012 Digital Bazaar, Inc. All rights reserved.
*/
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.
*
* @param input the JSON-LD object to normalize.
*
* @return the normalized JSON-LD object.
*/
function jsonld_normalize($input)
{
$p = new JsonLdProcessor();
return $p->normalize($input);
}
/**
* Removes the context from a JSON-LD object, expanding it to full-form.
*
* @param input the JSON-LD object to remove the context from.
*
* @return the context-neutral JSON-LD object.
*/
function jsonld_expand($input)
{
$p = new JsonLdProcessor();
return $p->expand(new stdClass(), null, $input);
}
/**
* Expands the given JSON-LD object and then compacts it using the
* given context.
*
* @param ctx the new context to use.
* @param input the input JSON-LD object.
*
* @return the output JSON-LD object.
*/
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;
}
/**
* Merges one context with another.
*
* @param ctx1 the context to overwrite/append to.
* @param ctx2 the new context to merge onto ctx1.
*
* @return the merged context.
*/
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;
}
/**
* 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.
*
* @return the expanded term as an absolute IRI.
*/
function jsonld_expand_term($ctx, $term)
{
return _expandTerm($ctx, $term);
}
/**
* 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.
*
* @return the compacted IRI as a term or prefix or the original IRI.
*/
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 @type, use keyword
if($rval === null and $iri === '@type')
{
$rval = _getKeywords($ctx)->{'@type'};
}
// 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 IRIs.
*/
class DuplicateIriFilter
{
public function __construct($iri)
{
$this->iri = $iri;
}
public function filter($e)
{
return (is_object($e) and property_exists($e, '@id') and
$e->{'@id'} === $this->iri);
}
}
/**
* 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 = false;
if(is_object($flattened) and property_exists($flattened, '@id'))
{
$duplicate = count(array_filter(
(array)$parent, array(
new DuplicateIriFilter($flattened->{'@id'}), '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);
}
}
/**
* A JSON-LD processor.
*/
class JsonLdProcessor
{
/**
* Constructs a JSON-LD processor.
*/
public function __construct()
{
}
/**
* 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;
// get JSON-LD keywords
$keywords = _getKeywords($ctx);
if($value === null)
{
// return null, but check coerce type to add to usedCtx
$rval = null;
$this->getCoerceType($ctx, $property, $usedCtx);
}
else if(is_array($value))
{
// recursively add compacted values to array
$rval = array();
foreach($value as $v)
{
$rval[] = $this->compact($ctx, $property, $v, $usedCtx);
}
}
// 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);
}
// 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);
}
}
}
}
else
{
// get coerce type
$coerce = $this->getCoerceType($ctx, $property, $usedCtx);
// 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;
}
// 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);
}
}
}
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;
// TODO: add data format error detection?
// value is null, nothing to expand
if($value === null)
{
$rval = null;
}
// 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);
}
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);
// get JSON-LD keywords
$keywords = _getKeywords($ctx);
// 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;
}
}
// 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)
{
$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');
}
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)
{
if(count($value->bnodes) > 0)
{
$bnode = $subjects->$key;
foreach($bnode as $p => $v)
{
if(strpos($p, '@') !== 0 and is_array($v))
{
usort($v, '_compareObjects');
$bnode->$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;
}
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 = '';
$first = true;
foreach($b as $p => $o)
{
if($p !== '@id')
{
if($first)
{
$first = false;
}
else
{
$rval .= '|';
}
// 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 . '"';
}
}
}
}
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;
}
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))
{
$rval = array();
$frames = $frame;
if(count($frames) == 0)
{
$frames[] = new stdClass();
}
}
else
{
$frames = array($frame);
$limit = 1;
}
// 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.');
}
// 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'}};
}
// 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;
}
// 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 a subject, do subframing
if(_isSubject($value))
{
$value = _subframe(
$subjects, $value, $frame, $embeds, $autoembed,
$parent, $parentKey, $options);
}
// 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'}));
// push any value that isn't a parentless reference
if(!($parent === null and $isRef))
{
$rval[] = $value;
}
}
}
}
return $rval;
}
?>