Added support for keyword aliasing.

This commit is contained in:
Dave Longley 2011-08-22 01:53:12 -04:00
commit 7e54fc304d

1139
jsonld.php
View file

@ -6,8 +6,6 @@
* *
* Copyright (c) 2011 Digital Bazaar, Inc. All rights reserved. * Copyright (c) 2011 Digital Bazaar, Inc. All rights reserved.
*/ */
define('__S', '@subject');
define('__T', '@type');
define('JSONLD_RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); define('JSONLD_RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
define('JSONLD_RDF_TYPE', JSONLD_RDF . 'type'); define('JSONLD_RDF_TYPE', JSONLD_RDF . 'type');
define('JSONLD_XSD', 'http://www.w3.org/2001/XMLSchema#'); define('JSONLD_XSD', 'http://www.w3.org/2001/XMLSchema#');
@ -28,7 +26,7 @@ function jsonld_normalize($input)
{ {
$p = new JsonLdProcessor(); $p = new JsonLdProcessor();
return $p->normalize($input); return $p->normalize($input);
}; }
/** /**
* Removes the context from a JSON-LD object, expanding it to full-form. * Removes the context from a JSON-LD object, expanding it to full-form.
@ -39,15 +37,9 @@ function jsonld_normalize($input)
*/ */
function jsonld_expand($input) function jsonld_expand($input)
{ {
$rval = null; $p = new JsonLdProcessor();
return $p->expand(new stdClass(), null, $input, false);
if($input !== null) }
{
$rval = _expand(new stdClass(), null, $input, false);
}
return $rval;
};
/** /**
* Expands the given JSON-LD object and then compacts it using the * Expands the given JSON-LD object and then compacts it using the
@ -86,7 +78,8 @@ function jsonld_compact($ctx, $input)
$ctxOut = new stdClass(); $ctxOut = new stdClass();
// compact // compact
$out = _compact(_clone($ctx), null, $value, $ctxOut); $p = new JsonLdProcessor();
$out = $p->compact(_clone($ctx), null, $value, $ctxOut);
// add context if used // add context if used
if(count(array_keys((array)$ctxOut)) > 0) if(count(array_keys((array)$ctxOut)) > 0)
@ -310,7 +303,7 @@ function jsonld_frame($input, $frame, $options=null)
$subjects = new stdClass(); $subjects = new stdClass();
foreach($input as $i) foreach($input as $i)
{ {
$subjects->{$i->{__S}->{'@iri'}} = $i; $subjects->{$i->{'@subject'}->{'@iri'}} = $i;
} }
// frame input // frame input
@ -326,149 +319,42 @@ function jsonld_frame($input, $frame, $options=null)
} }
/** /**
* Compacts an IRI into a term or CURIE if it can be. IRIs will not be * Gets the keywords from a context.
* compacted to relative IRIs if they match the given context's default
* vocabulary.
* *
* @param ctx the context to use. * @param ctx the context.
* @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 CURIE or the original IRI. * @return the keywords.
*/ */
function _compactIri($ctx, $iri, $usedCtx) function _getKeywords($ctx)
{ {
$rval = null; // TODO: reduce calls to this function by caching keywords in processor
// state
// check the context for a term that could shorten the IRI $rval = (object)array(
// (give preference to terms over CURIEs) '@datatype' => '@datatype',
foreach($ctx as $key => $value) '@iri' => '@iri',
{ '@language' => '@language',
// skip special context keys (start with '@') '@literal' => '@literal',
if(strlen($key) > 0 and $key[0] !== '@') '@subject' => '@subject',
{ '@type' => '@type'
// compact to a term );
if($iri === $ctx->$key)
{
$rval = $key;
if($usedCtx !== null)
{
$usedCtx->$key = $ctx->$key;
}
break;
}
}
}
// term not found, if term is rdf type, use built-in keyword if($ctx)
if($rval === null and $iri === JSONLD_RDF_TYPE)
{
$rval = __T;
}
// term not found, check the context for a CURIE prefix
if($rval === null)
{ {
// gather keyword aliases from context
$keywords = new stdClass();
foreach($ctx as $key => $value) foreach($ctx as $key => $value)
{ {
// skip special context keys (start with '@') if(is_string($value) and property_exists($rval, $value))
if(strlen($key) > 0 and $key[0] !== '@')
{ {
// see if IRI begins with the next IRI from the context $keywords->{$value} = $key;
$ctxIri = $ctx->$key;
$idx = strpos($iri, $ctxIri);
// compact to a CURIE
if($idx === 0 and strlen($iri) > strlen($ctxIri))
{
$rval = $key . ':' . substr($iri, strlen($ctxIri));
if($usedCtx !== null)
{
$usedCtx->$key = $ctxIri;
}
break;
}
} }
} }
}
// could not compact IRI // overwrite keywords
if($rval === null) foreach($keywords as $key => $value)
{
$rval = $iri;
}
return $rval;
}
/**
* Expands a term into an absolute IRI. The term may be a regular term, a
* CURIE, 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;
// 1. If the property has a colon, then it is a CURIE or an absolute IRI:
$idx = strpos($term, ':');
if($idx !== false)
{
// get the potential CURIE prefix
$prefix = substr($term, 0, $idx);
// 1.1. See if the prefix is in the context:
if(property_exists($ctx, $prefix))
{ {
// prefix found, expand property to absolute IRI $rval->$key = $value;
$rval = $ctx->$prefix . substr($term, $idx + 1);
if($usedCtx !== null)
{
$usedCtx->$prefix = $ctx->$prefix;
}
}
// 1.2. Prefix is not in context, property is already an absolute IRI:
else
{
$rval = $term;
}
}
// 2. If the property is in the context, then it's a term.
else if(property_exists($ctx, $term))
{
$rval = $ctx->$term;
if($usedCtx !== null)
{
$usedCtx->$term = $rval;
}
}
// 3. The property is the special-case subject.
else if($term === __S)
{
$rval = __S;
}
// 4. The property is the special-case rdf type.
else if($term === __T)
{
$rval = JSONLD_RDF_TYPE;
}
// 5. The property is a relative IRI, prepend the default vocab.
else
{
$rval = $term;
if(property_exists($ctx, '@vocab'))
{
$rval = $ctx->{'@vocab'} . $rval;
if($usedCtx !== null)
{
$usedCtx->{'@vocab'} = $ctx->{'@vocab'};
}
} }
} }
@ -541,67 +427,65 @@ function _clone($value)
} }
/** /**
* Gets the coerce type for the given property. * Compacts an IRI into a term or CURIE 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 ctx the context to use.
* @param property the property to get the coerced type for. * @param iri the IRI to compact.
* @param usedCtx a context to update if a value was used from "ctx". * @param usedCtx a context to update if a value was used from "ctx".
* *
* @return the coerce type, null for none. * @return the compacted IRI as a term or CURIE or the original IRI.
*/ */
function _getCoerceType($ctx, $property, $usedCtx) function _compactIri($ctx, $iri, $usedCtx)
{ {
$rval = null; $rval = null;
// get expanded property // check the context for a term that could shorten the IRI
$p = _expandTerm($ctx, $property, null); // (give preference to terms over CURIEs)
foreach($ctx as $key => $value)
// built-in type coercion JSON-LD-isms
if($p === __S or $p === JSONLD_RDF_TYPE)
{ {
$rval = JSONLD_XSD_ANY_URI; // skip special context keys (start with '@')
} if(strlen($key) > 0 and $key[0] !== '@')
// check type coercion for property
else if(property_exists($ctx, '@coerce'))
{
// force compacted property
$p = _compactIri($ctx, $p, null);
foreach($ctx->{'@coerce'} as $type => $props)
{ {
// get coerced properties (normalize to an array) // compact to a term
if(!is_array($props)) if($iri === $ctx->$key)
{ {
$props = array($props); $rval = $key;
} if($usedCtx !== null)
// look for the property in the array
foreach($props as $prop)
{
// property found
if($prop === $p)
{ {
$rval = _expandTerm($ctx, $type, $usedCtx); $usedCtx->$key = $ctx->$key;
}
break;
}
}
}
// term not found, if term is rdf type, use built-in keyword
if($rval === null and $iri === JSONLD_RDF_TYPE)
{
$rval = _getKeywords($ctx)->{'@type'};
}
// term not found, check the context for a CURIE 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 = $ctx->$key;
$idx = strpos($iri, $ctxIri);
// compact to a CURIE
if($idx === 0 and strlen($iri) > strlen($ctxIri))
{
$rval = $key . ':' . substr($iri, strlen($ctxIri));
if($usedCtx !== null) if($usedCtx !== null)
{ {
if(!property_exists($usedCtx, '@coerce')) $usedCtx->$key = $ctxIri;
{
$usedCtx->{'@coerce'} = new stdClass();
}
if(!property_exists($usedCtx->{'@coerce'}, $type))
{
$usedCtx->{'@coerce'}->$type = $p;
}
else
{
$c = $usedCtx->{'@coerce'}->$type;
if(is_array($c) and in_array($p, $c) or
is_string($c) and $c !== $p)
{
_setProperty($usedCtx->{'@coerce'}, $type, $p);
}
}
} }
break; break;
} }
@ -609,316 +493,87 @@ function _getCoerceType($ctx, $property, $usedCtx)
} }
} }
// could not compact IRI
if($rval === null)
{
$rval = $iri;
}
return $rval; return $rval;
} }
/** /**
* Recursively compacts a value. This method will compact IRIs to CURIEs or * Expands a term into an absolute IRI. The term may be a regular term, a
* terms and do reverse type coercion to compact a value. * CURIE, a relative IRI, or an absolute IRI. In any case, the associated
* absolute IRI will be returned.
* *
* @param ctx the context to use. * @param ctx the context to use.
* @param property the property that points to the value, NULL for none. * @param term the term to expand.
* @param value the value to compact.
* @param usedCtx a context to update if a value was used from "ctx". * @param usedCtx a context to update if a value was used from "ctx".
* *
* @return the compacted value. * @return the expanded term as an absolute IRI.
*/ */
function _compact($ctx, $property, $value, $usedCtx) function _expandTerm($ctx, $term, $usedCtx)
{ {
$rval; $rval;
if($value === null) // get JSON-LD keywords
$keywords = _getKeywords($ctx);
// 1. If the property has a colon, then it is a CURIE or an absolute IRI:
$idx = strpos($term, ':');
if($idx !== false)
{ {
// return null, but check coerce type to add to usedCtx // get the potential CURIE prefix
$rval = null; $prefix = substr($term, 0, $idx);
_getCoerceType($ctx, $property, $usedCtx);
} // 1.1. See if the prefix is in the context:
else if(is_array($value)) if(property_exists($ctx, $prefix))
{
// recursively add compacted values to array
$rval = array();
foreach($value as $v)
{ {
$rval[] = _compact($ctx, $property, $v, $usedCtx); // prefix found, expand property to absolute IRI
} $rval = $ctx->$prefix . substr($term, $idx + 1);
} if($usedCtx !== null)
// graph literal/disjoint graph
else if(
is_object($value) and
property_exists($value, __S) and is_array($value->{__S}))
{
$rval = new stdClass();
$rval->{__S} = _compact($ctx, $property, $value->{__S}, $usedCtx);
}
// value has sub-properties if it doesn't define a literal or IRI value
else if(
is_object($value) and
!property_exists($value, '@literal') and
!property_exists($value, '@iri'))
{
// 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 $usedCtx->$prefix = $ctx->$prefix;
// properties if the property actually compacted
$p = _compactIri($ctx, $key, $usedCtx);
if($p !== $key or !property_exists($rval, $p))
{
$rval->$p = _compact($ctx, $key, $v, $usedCtx);
}
} }
} }
// 1.2. Prefix is not in context, property is already an absolute IRI:
else
{
$rval = $term;
}
} }
// 2. If the property is in the context, then it's a term.
else if(property_exists($ctx, $term))
{
$rval = $ctx->$term;
if($usedCtx !== null)
{
$usedCtx->$term = $rval;
}
}
// 3. The property is the special-case subject.
else if($term === $keywords->{'@subject'})
{
$rval = $keywords->{'@subject'};
}
// 4. The property is the special-case rdf type.
else if($term === $keywords->{'@type'})
{
$rval = JSONLD_RDF_TYPE;
}
// 5. The property is a relative IRI, prepend the default vocab.
else else
{ {
// get coerce type $rval = $term;
$coerce = _getCoerceType($ctx, $property, $usedCtx); if(property_exists($ctx, '@vocab'))
// 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 $rval = $ctx->{'@vocab'} . $rval;
if(!property_exists($value, '@language')) if($usedCtx !== null)
{ {
// datatype must match coerce type if specified $usedCtx->{'@vocab'} = $ctx->{'@vocab'};
if(property_exists($value, '@datatype'))
{
$type = $value->{'@datatype'};
}
// datatype is IRI
else if(property_exists($value, '@iri'))
{
$type = JSONLD_XSD_ANY_URI;
}
// 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 datatype does not match.');
}
// do reverse type-coercion
else
{
if(is_object($value))
{
if(property_exists($value, '@iri'))
{
$rval = $value->{'@iri'};
}
else if(property_exists($value, '@literal'))
{
$rval = $value->{'@literal'};
}
}
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 copy value
else
{
$rval = _clone($value);
}
// compact IRI
if($type === JSONLD_XSD_ANY_URI)
{
if(is_object($rval))
{
$rval->{'@iri'} = _compactIri($ctx, $rval->{'@iri'}, $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.
* @param expandSubjects true to expand subjects (normalize), false not to.
*
* @return the expanded value.
*/
function _expand($ctx, $property, $value, $expandSubjects)
{
$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[] = _expand($ctx, $property, $v, $expandSubjects);
}
}
else if(is_object($value))
{
// value has sub-properties if it doesn't define a literal or IRI value
if(!(property_exists($value, '@literal') or
property_exists($value, '@iri')))
{
// 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),
_expand($ctx, $key, $v, $expandSubjects));
}
}
}
// value is already expanded
else
{
$rval = _clone($value);
}
}
else
{
// do type coercion
$coerce = _getCoerceType($ctx, $property, null);
// 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;
}
}
// coerce to appropriate datatype, only expand subjects if requested
if($coerce !== null and ($property !== __S or $expandSubjects))
{
$rval = new stdClass();
// expand IRI
if($coerce === JSONLD_XSD_ANY_URI)
{
$rval->{'@iri'} = _expandTerm($ctx, $value, null);
}
// other datatype
else
{
$rval->{'@datatype'} = $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->{'@literal'} = '' . $value;
}
}
// nothing to coerce
else
{
$rval = '' . $value;
}
} }
return $rval; return $rval;
@ -933,9 +588,9 @@ function _isNamedBlankNode($v)
{ {
// look for "_:" at the beginning of the subject // look for "_:" at the beginning of the subject
return ( return (
is_object($v) and property_exists($v, __S) and is_object($v) and property_exists($v, '@subject') and
property_exists($v->{__S}, '@iri') and property_exists($v->{'@subject'}, '@iri') and
_isBlankNodeIri($v->{__S}->{'@iri'})); _isBlankNodeIri($v->{'@subject'}->{'@iri'}));
} }
function _isBlankNode($v) function _isBlankNode($v)
@ -944,7 +599,7 @@ function _isBlankNode($v)
return ( return (
is_object($v) and is_object($v) and
!(property_exists($v, '@iri') or property_exists($v, '@literal')) and !(property_exists($v, '@iri') or property_exists($v, '@literal')) and
(!property_exists($v, __S) or _isNamedBlankNode($v))); (!property_exists($v, '@subject') or _isNamedBlankNode($v)));
} }
/** /**
@ -1195,17 +850,17 @@ function _collectSubjects($input, $subjects, $bnodes)
} }
else if(is_object($input)) else if(is_object($input))
{ {
if(property_exists($input, __S)) if(property_exists($input, '@subject'))
{ {
// graph literal // graph literal
if(is_array($input->{__S})) if(is_array($input->{'@subject'}))
{ {
_collectSubjects($input->{__S}, $subjects, $bnodes); _collectSubjects($input->{'@subject'}, $subjects, $bnodes);
} }
// named subject // named subject
else else
{ {
$subjects->{$input->{__S}->{'@iri'}} = $input; $subjects->{$input->{'@subject'}->{'@iri'}} = $input;
} }
} }
// unnamed blank node // unnamed blank node
@ -1268,7 +923,7 @@ function _flatten($parent, $parentProperty, $value, $subjects)
else if(is_object($value)) else if(is_object($value))
{ {
// graph literal/disjoint graph // graph literal/disjoint graph
if(property_exists($value, __S) and is_array($value->{__S})) if(property_exists($value, '@subject') and is_array($value->{'@subject'}))
{ {
// cannot flatten embedded graph literals // cannot flatten embedded graph literals
if($parent !== null) if($parent !== null)
@ -1277,7 +932,7 @@ function _flatten($parent, $parentProperty, $value, $subjects)
} }
// top-level graph literal // top-level graph literal
foreach($value->{__S} as $k => $v) foreach($value->{'@subject'} as $k => $v)
{ {
_flatten($parent, $parentProperty, $v, $subjects); _flatten($parent, $parentProperty, $v, $subjects);
} }
@ -1293,18 +948,18 @@ function _flatten($parent, $parentProperty, $value, $subjects)
else else
{ {
// create or fetch existing subject // create or fetch existing subject
if(property_exists($subjects, $value->{__S}->{'@iri'})) if(property_exists($subjects, $value->{'@subject'}->{'@iri'}))
{ {
// FIXME: __S might be a graph literal (as {}) // FIXME: '@subject' might be a graph literal (as {})
$subject = $subjects->{$value->{__S}->{'@iri'}}; $subject = $subjects->{$value->{'@subject'}->{'@iri'}};
} }
else else
{ {
$subject = new stdClass(); $subject = new stdClass();
if(property_exists($value, __S)) if(property_exists($value, '@subject'))
{ {
// FIXME: __S might be a graph literal (as {}) // FIXME: '@subject' might be a graph literal (as {})
$subjects->{$value->{__S}->{'@iri'}} = $subject; $subjects->{$value->{'@subject'}->{'@iri'}} = $subject;
} }
} }
$flattened = $subject; $flattened = $subject;
@ -1352,13 +1007,13 @@ function _flatten($parent, $parentProperty, $value, $subjects)
// add flattened value to parent // add flattened value to parent
if($flattened !== null and $parent !== null) if($flattened !== null and $parent !== null)
{ {
// remove top-level __s for subjects // remove top-level '@subject' for subjects
// 'http://mypredicate': {'@subject': {'@iri': 'http://mysubject'}} // 'http://mypredicate': {'@subject': {'@iri': 'http://mysubject'}}
// becomes // becomes
// 'http://mypredicate': {'@iri': 'http://mysubject'} // 'http://mypredicate': {'@iri': 'http://mysubject'}
if(is_object($flattened) and property_exists($flattened, __S)) if(is_object($flattened) and property_exists($flattened, '@subject'))
{ {
$flattened = $flattened->{__S}; $flattened = $flattened->{'@subject'};
} }
if($parent instanceof ArrayObject) if($parent instanceof ArrayObject)
@ -1381,7 +1036,7 @@ function _flatten($parent, $parentProperty, $value, $subjects)
$parent->$parentProperty = $flattened; $parent->$parentProperty = $flattened;
} }
} }
}; }
/** /**
* A MappingBuilder is used to build a mapping of existing blank node names * A MappingBuilder is used to build a mapping of existing blank node names
@ -1502,7 +1157,7 @@ function _compareSerializations($s1, $s2)
*/ */
function _compareIris($a, $b) function _compareIris($a, $b)
{ {
return _compare($a->{__S}->{'@iri'}, $b->{__S}->{'@iri'}); return _compare($a->{'@subject'}->{'@iri'}, $b->{'@subject'}->{'@iri'});
} }
/** /**
@ -1534,7 +1189,7 @@ class MappingKeySorter
} }
/** /**
* A container for JSON-LD processing state. * A JSON-LD processor.
*/ */
class JsonLdProcessor class JsonLdProcessor
{ {
@ -1543,9 +1198,355 @@ class JsonLdProcessor
*/ */
public function __construct() public function __construct()
{ {
$this->ng = new stdClass(); }
$this->ng->tmp = null;
$this->ng->c14n = null; /**
* Recursively compacts a value. This method will compact IRIs to CURIEs 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, '@subject') and
is_array($value->{'@subject'}))
{
$rval = new stdClass();
$rval->{$keywords->{'@subject'}} = $this->compact(
$ctx, $property, $value->{'@subject'}, $usedCtx);
}
// value has sub-properties if it doesn't define a literal or IRI value
else if(
is_object($value) and
!property_exists($value, '@literal') and
!property_exists($value, '@iri'))
{
// 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'))
{
// datatype must match coerce type if specified
if(property_exists($value, '@datatype'))
{
$type = $value->{'@datatype'};
}
// datatype is IRI
else if(property_exists($value, '@iri'))
{
$type = JSONLD_XSD_ANY_URI;
}
// 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 datatype does not match.');
}
// do reverse type-coercion
else
{
if(is_object($value))
{
if(property_exists($value, '@iri'))
{
$rval = $value->{'@iri'};
}
else if(property_exists($value, '@literal'))
{
$rval = $value->{'@literal'};
}
}
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 === JSONLD_XSD_ANY_URI)
{
if(is_object($rval))
{
$rval->{$keywords->{'@iri'}} = _compactIri(
$ctx, $rval->{$keywords->{'@iri'}}, $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.
* @param expandSubjects true to expand subjects (normalize), false not to.
*
* @return the expanded value.
*/
public function expand($ctx, $property, $value, $expandSubjects)
{
$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, $expandSubjects);
}
}
else if(is_object($value))
{
// if value has a context, use it
if(property_exists($value, '@context'))
{
$ctx = jsonld_merge_contexts($ctx, $value->{'@context'});
}
// get JSON-LD keywords
$keywords = _getKeywords($ctx);
// value has sub-properties if it doesn't define a literal or IRI value
if(!(property_exists($value, $keywords->{'@literal'}) or
property_exists($value, $keywords->{'@iri'})))
{
// 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, $expandSubjects));
}
}
}
// only need to expand key words
else
{
$rval = new stdClass();
if(property_exists($value, $keywords->{'@iri'}))
{
$rval->{'@iri'} = $value->{$keywords->{'@iri'}};
}
else
{
$rval->{'@literal'} = $value->{$keywords->{'@literal'}};
if(property_exists($value, $keywords->{'@language'}))
{
$rval->{'@language'} = $value->{$keywords->{'@language'}};
}
else if(property_exists($value, $keywords->{'@datatype'}))
{
$rval->{'@datatype'} = $value->{$keywords->{'@datatype'}};
}
}
}
}
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;
}
}
// coerce to appropriate datatype, only expand subjects if requested
if($coerce !== null and (
$property !== $keywords->{'@subject'} or $expandSubjects))
{
$rval = new stdClass();
// expand IRI
if($coerce === JSONLD_XSD_ANY_URI)
{
$rval->{'@iri'} = _expandTerm($ctx, $value, null);
}
// other datatype
else
{
$rval->{'@datatype'} = $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->{'@literal'} = '' . $value;
}
}
// nothing to coerce
else
{
$rval = '' . $value;
}
}
return $rval;
} }
/** /**
@ -1563,8 +1564,13 @@ class JsonLdProcessor
if($input !== null) if($input !== null)
{ {
// create name generator state
$this->ng = new stdClass();
$this->ng->tmp = null;
$this->ng->c14n = null;
// expand input // expand input
$expanded = _expand(new stdClass(), null, $input, true); $expanded = $this->expand(new stdClass(), null, $input, true);
// assign names to unnamed bnodes // assign names to unnamed bnodes
$this->nameBlankNodes($expanded); $this->nameBlankNodes($expanded);
@ -1596,6 +1602,85 @@ class JsonLdProcessor
return $rval; 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 === '@subject' or $p === JSONLD_RDF_TYPE)
{
$rval = JSONLD_XSD_ANY_URI;
}
// check type coercion for property
else if(property_exists($ctx, '@coerce'))
{
// force compacted property
$p = _compactIri($ctx, $p, null);
foreach($ctx->{'@coerce'} as $type => $props)
{
// get coerced properties (normalize to an array)
if(!is_array($props))
{
$props = array($props);
}
// look for the property in the array
foreach($props as $prop)
{
// property found
if($prop === $p)
{
$rval = _expandTerm($ctx, $type, $usedCtx);
// '@iri' is shortcut for JSONLD_XSD_ANY_URI
if($rval === '@iri')
{
$rval = JSONLD_XSD_ANY_URI;
}
if($usedCtx !== null)
{
if(!property_exists($usedCtx, '@coerce'))
{
$usedCtx->{'@coerce'} = new stdClass();
}
if(!property_exists($usedCtx->{'@coerce'}, $type))
{
$usedCtx->{'@coerce'}->$type = $p;
}
else
{
$c = $usedCtx->{'@coerce'}->$type;
if(is_array($c) and in_array($p, $c) or
is_string($c) and $c !== $p)
{
_setProperty($usedCtx->{'@coerce'}, $type, $p);
}
}
}
break;
}
}
}
}
return $rval;
}
/** /**
* Assigns unique names to blank nodes that are unnamed in the given input. * Assigns unique names to blank nodes that are unnamed in the given input.
* *
@ -1614,12 +1699,12 @@ class JsonLdProcessor
// uniquely name all unnamed bnodes // uniquely name all unnamed bnodes
foreach($bnodes as $i => $bnode) foreach($bnodes as $i => $bnode)
{ {
if(!property_exists($bnode, __S)) if(!property_exists($bnode, '@subject'))
{ {
// generate names until one is unique // generate names until one is unique
while(property_exists($subjects, $ng->next())); while(property_exists($subjects, $ng->next()));
$bnode->{__S} = new stdClass(); $bnode->{'@subject'} = new stdClass();
$bnode->{__S}->{'@iri'} = $ng->current(); $bnode->{'@subject'}->{'@iri'} = $ng->current();
$subjects->{$ng->current()} = $bnode; $subjects->{$ng->current()} = $bnode;
} }
} }
@ -1634,10 +1719,10 @@ class JsonLdProcessor
*/ */
public function renameBlankNode($b, $id) public function renameBlankNode($b, $id)
{ {
$old = $b->{__S}->{'@iri'}; $old = $b->{'@subject'}->{'@iri'};
// update bnode IRI // update bnode IRI
$b->{__S}->{'@iri'} = $id; $b->{'@subject'}->{'@iri'} = $id;
// update subjects map // update subjects map
$subjects = $this->subjects; $subjects = $this->subjects;
@ -1721,7 +1806,7 @@ class JsonLdProcessor
$bnodes = array(); $bnodes = array();
foreach($input as $v) foreach($input as $v)
{ {
$iri = $v->{__S}->{'@iri'}; $iri = $v->{'@subject'}->{'@iri'};
$subjects->$iri = $v; $subjects->$iri = $v;
$edges->refs->$iri = new stdClass(); $edges->refs->$iri = new stdClass();
$edges->refs->$iri->all = array(); $edges->refs->$iri->all = array();
@ -1746,13 +1831,13 @@ class JsonLdProcessor
// and initialize serializations // and initialize serializations
foreach($bnodes as $i => $bnode) foreach($bnodes as $i => $bnode)
{ {
$iri = $bnode->{__S}->{'@iri'}; $iri = $bnode->{'@subject'}->{'@iri'};
if($c14n->inNamespace($iri)) if($c14n->inNamespace($iri))
{ {
// generate names until one is unique // generate names until one is unique
while(property_exists($subjects, $ngTmp->next())); while(property_exists($subjects, $ngTmp->next()));
$this->renameBlankNode($bnode, $ngTmp->current()); $this->renameBlankNode($bnode, $ngTmp->current());
$iri = $bnode->{__S}->{'@iri'}; $iri = $bnode->{'@subject'}->{'@iri'};
} }
$this->serializations->$iri = new stdClass(); $this->serializations->$iri = new stdClass();
$this->serializations->$iri->props = null; $this->serializations->$iri->props = null;
@ -1766,7 +1851,7 @@ class JsonLdProcessor
// name all bnodes according to the first bnode's relation mappings // name all bnodes according to the first bnode's relation mappings
$bnode = array_shift($bnodes); $bnode = array_shift($bnodes);
$iri = $bnode->{__S}->{'@iri'}; $iri = $bnode->{'@subject'}->{'@iri'};
$dirs = array('props', 'refs'); $dirs = array('props', 'refs');
foreach($dirs as $dir) foreach($dirs as $dir)
{ {
@ -1802,7 +1887,7 @@ class JsonLdProcessor
$bnodes = array(); $bnodes = array();
foreach($tmp as $i => $b) foreach($tmp as $i => $b)
{ {
$iriB = $b->{__S}->{'@iri'}; $iriB = $b->{'@subject'}->{'@iri'};
if(!$c14n->inNamespace($iriB)) if(!$c14n->inNamespace($iriB))
{ {
// mark serializations related to the named bnodes as dirty // mark serializations related to the named bnodes as dirty
@ -2154,8 +2239,8 @@ class JsonLdProcessor
$rval = 0; $rval = 0;
// compare IRIs // compare IRIs
$iriA = $a->{__S}->{'@iri'}; $iriA = $a->{'@subject'}->{'@iri'};
$iriB = $b->{__S}->{'@iri'}; $iriB = $b->{'@subject'}->{'@iri'};
if($iriA === $iriB) if($iriA === $iriB)
{ {
$rval = 0; $rval = 0;
@ -2257,8 +2342,8 @@ class JsonLdProcessor
// step #4 // step #4
if($rval === 0) if($rval === 0)
{ {
$edgesA = $this->edges->refs->{$a->{__S}->{'@iri'}}->all; $edgesA = $this->edges->refs->{$a->{'@subject'}->{'@iri'}}->all;
$edgesB = $this->edges->refs->{$b->{__S}->{'@iri'}}->all; $edgesB = $this->edges->refs->{$b->{'@subject'}->{'@iri'}}->all;
$edgesALen = count($edgesA); $edgesALen = count($edgesA);
$rval = _compare($edgesALen, count($edgesB)); $rval = _compare($edgesALen, count($edgesB));
} }
@ -2347,7 +2432,7 @@ class JsonLdProcessor
{ {
foreach($subject as $key => $object) foreach($subject as $key => $object)
{ {
if($key !== __S) if($key !== '@subject')
{ {
// normalize to array for single codepath // normalize to array for single codepath
$tmp = !is_array($object) ? array($object) : $object; $tmp = !is_array($object) ? array($object) : $object;
@ -2405,7 +2490,7 @@ function _isType($input, $frame)
// check if type(s) are specified in frame and input // check if type(s) are specified in frame and input
$type = JSONLD_RDF_TYPE; $type = JSONLD_RDF_TYPE;
if(property_exists($frame, JSONLD_RDF_TYPE) and if(property_exists($frame, JSONLD_RDF_TYPE) and
is_object($input) and property_exists($input, __S) and is_object($input) and property_exists($input, '@subject') and
property_exists($input, JSONLD_RDF_TYPE)) property_exists($input, JSONLD_RDF_TYPE))
{ {
$tmp = is_array($input->{JSONLD_RDF_TYPE}) ? $tmp = is_array($input->{JSONLD_RDF_TYPE}) ?
@ -2465,7 +2550,7 @@ function _isDuckType($input, $frame)
$rval = true; $rval = true;
} }
// input must be a subject with all the given properties // input must be a subject with all the given properties
else if(is_object($input) and property_exists($input, __S)) else if(is_object($input) and property_exists($input, '@subject'))
{ {
$rval = true; $rval = true;
foreach($props as $prop) foreach($props as $prop)
@ -2564,25 +2649,25 @@ function _frame($subjects, $input, $frame, $embeds, $options)
if(!$embedOn) if(!$embedOn)
{ {
// if value is a subject, only use subject IRI as reference // if value is a subject, only use subject IRI as reference
if(is_object($value) and property_exists($value, __S)) if(is_object($value) and property_exists($value, '@subject'))
{ {
$value = $value->{__S}; $value = $value->{'@subject'};
} }
} }
else if( else if(
is_object($value) and property_exists($value, __S) and is_object($value) and property_exists($value, '@subject') and
property_exists($embeds, $value->{__S}->{'@iri'})) property_exists($embeds, $value->{'@subject'}->{'@iri'}))
{ {
// TODO: possibly support multiple embeds in the future ... and // TODO: possibly support multiple embeds in the future ... and
// instead only prevent cycles? // instead only prevent cycles?
throw new Exception( throw new Exception(
'Multiple embeds of the same subject is not supported. ' . 'Multiple embeds of the same subject is not supported. ' .
'subject=' . $value->{__S}->{'@iri'}); 'subject=' . $value->{'@subject'}->{'@iri'});
} }
// if value is a subject, do embedding and subframing // if value is a subject, do embedding and subframing
else if(is_object($value) and property_exists($value, __S)) else if(is_object($value) and property_exists($value, '@subject'))
{ {
$embeds->{$value->{__S}->{'@iri'}} = true; $embeds->{$value->{'@subject'}->{'@iri'}} = true;
// if explicit is on, remove keys from value that aren't in frame // if explicit is on, remove keys from value that aren't in frame
$explicitOn = property_exists($frame, '@explicit') ? $explicitOn = property_exists($frame, '@explicit') ?
@ -2592,7 +2677,7 @@ function _frame($subjects, $input, $frame, $embeds, $options)
foreach($value as $key => $v) foreach($value as $key => $v)
{ {
// do not remove subject or any key in the frame // do not remove subject or any key in the frame
if($key !== __S and !property_exists($frame, $key)) if($key !== '@subject' and !property_exists($frame, $key))
{ {
unset($value->$key); unset($value->$key);
} }