Handle scalars before objects.

This commit is contained in:
Dave Longley 2013-09-13 13:17:40 -04:00
parent d1501c7c08
commit 12a9ccc9e8

View file

@ -2102,351 +2102,352 @@ class JsonLdProcessor {
return $rval; return $rval;
} }
// recursively expand object if(!is_object($element)) {
if(is_object($element)) { // drop top-level scalars that are not in lists
// if element has a context, process it if(!$inside_list &&
if(property_exists($element, '@context')) { ($active_property === null ||
$active_ctx = $this->_processContext( $this->_expandIri($active_ctx, $active_property,
$active_ctx, $element->{'@context'}, $options); array('vocab' => true)) === '@graph')) {
return null;
} }
// expand the active property // expand element according to value expansion rules
$expanded_active_property = $this->_expandIri( return $this->_expandValue($active_ctx, $active_property, $element);
$active_ctx, $active_property, array('vocab' => true)); }
$rval = new stdClass(); // recursively expand object:
$keys = array_keys((array)$element);
sort($keys);
foreach($keys as $key) {
$value = $element->{$key};
if($key === '@context') { // if element has a context, process it
continue; if(property_exists($element, '@context')) {
} $active_ctx = $this->_processContext(
$active_ctx, $element->{'@context'}, $options);
}
// get term definition for key // expand the active property
if(property_exists($active_ctx->mappings, $key)) { $expanded_active_property = $this->_expandIri(
$mapping = $active_ctx->mappings->{$key}; $active_ctx, $active_property, array('vocab' => true));
}
else {
$mapping = null;
}
// expand key to IRI $rval = new stdClass();
$expanded_property = $this->_expandIri( $keys = array_keys((array)$element);
$active_ctx, $key, array('vocab' => true)); sort($keys);
foreach($keys as $key) {
$value = $element->{$key};
// drop non-absolute IRI keys that aren't keywords if($key === '@context') {
if($expanded_property === null || continue;
!(self::_isAbsoluteIri($expanded_property) || }
self::_isKeyword($expanded_property))) {
continue;
}
if(self::_isKeyword($expanded_property) && // get term definition for key
$expanded_active_property === '@reverse') { if(property_exists($active_ctx->mappings, $key)) {
$mapping = $active_ctx->mappings->{$key};
}
else {
$mapping = null;
}
// expand key to IRI
$expanded_property = $this->_expandIri(
$active_ctx, $key, array('vocab' => true));
// drop non-absolute IRI keys that aren't keywords
if($expanded_property === null ||
!(self::_isAbsoluteIri($expanded_property) ||
self::_isKeyword($expanded_property))) {
continue;
}
if(self::_isKeyword($expanded_property) &&
$expanded_active_property === '@reverse') {
throw new JsonLdException(
'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' .
'property.',
'jsonld.SyntaxError', array('value' => $value));
}
// syntax error if @id is not a string
if($expanded_property === '@id' && !is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@id" value must a string.',
'jsonld.SyntaxError', array('value' => $value));
}
// validate @type value
if($expanded_property === '@type') {
$this->_validateTypeValue($value);
}
// @graph must be an array or an object
if($expanded_property === '@graph' &&
!(is_object($value) || is_array($value))) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@value" value must not be an ' .
'object or an array.',
'jsonld.SyntaxError', array('value' => $value));
}
// @value must not be an object or an array
if($expanded_property === '@value' &&
(is_object($value) || is_array($value))) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@value" value must not be an ' .
'object or an array.',
'jsonld.SyntaxError', array('value' => $value));
}
// @language must be a string
if($expanded_property === '@language' && !is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@language" value must not be a string.',
'jsonld.SyntaxError', array('value' => $value));
// ensure language value is lowercase
$value = strtolower($value);
}
// @index must be a string
if($expanded_property === '@index') {
if(!is_string($value)) {
throw new JsonLdException( throw new JsonLdException(
'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' . 'Invalid JSON-LD syntax; "@index" value must be a string.',
'property.', 'jsonld.SyntaxError', array('value' => $value));
}
}
// @reverse must be an object
if($expanded_property === '@reverse') {
if(!is_object($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@reverse" value must be an object.',
'jsonld.SyntaxError', array('value' => $value)); 'jsonld.SyntaxError', array('value' => $value));
} }
// syntax error if @id is not a string $expanded_value = $this->_expand(
if($expanded_property === '@id' && !is_string($value)) { $active_ctx, '@reverse', $value, $options, $inside_list);
throw new JsonLdException(
'Invalid JSON-LD syntax; "@id" value must a string.',
'jsonld.SyntaxError', array('value' => $value));
}
// validate @type value // properties double-reversed
if($expanded_property === '@type') { if(property_exists($expanded_value, '@reverse')) {
$this->_validateTypeValue($value); foreach($expanded_value->{'@reverse'} as $rproperty => $rvalue) {
}
// @graph must be an array or an object
if($expanded_property === '@graph' &&
!(is_object($value) || is_array($value))) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@value" value must not be an ' .
'object or an array.',
'jsonld.SyntaxError', array('value' => $value));
}
// @value must not be an object or an array
if($expanded_property === '@value' &&
(is_object($value) || is_array($value))) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@value" value must not be an ' .
'object or an array.',
'jsonld.SyntaxError', array('value' => $value));
}
// @language must be a string
if($expanded_property === '@language' && !is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@language" value must not be a string.',
'jsonld.SyntaxError', array('value' => $value));
// ensure language value is lowercase
$value = strtolower($value);
}
// @index must be a string
if($expanded_property === '@index') {
if(!is_string($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@index" value must be a string.',
'jsonld.SyntaxError', array('value' => $value));
}
}
// @reverse must be an object
if($expanded_property === '@reverse') {
if(!is_object($value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@reverse" value must be an object.',
'jsonld.SyntaxError', array('value' => $value));
}
$expanded_value = $this->_expand(
$active_ctx, '@reverse', $value, $options, $inside_list);
// properties double-reversed
if(property_exists($expanded_value, '@reverse')) {
foreach($expanded_value->{'@reverse'} as $rproperty => $rvalue) {
self::addValue(
$rval, $rproperty, $rvalue, array('propertyIsArray' => true));
}
}
// FIXME: can this be merged with code below to simplify?
// merge in all reversed properties
if(property_exists($rval, '@reverse')) {
$reverse_map = $rval->{'@reverse'};
}
else {
$reverse_map = null;
}
foreach($expanded_value as $property => $items) {
if($property === '@reverse') {
continue;
}
if($reverse_map === null) {
$reverse_map = $rval->{'@reverse'} = new stdClass();
}
self::addValue( self::addValue(
$reverse_map, $property, array(), $rval, $rproperty, $rvalue, array('propertyIsArray' => true));
array('propertyIsArray' => true));
foreach($items as $item) {
if(self::_isValue($item) || self::_isList($item)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
'@value or an @list.',
'jsonld.SyntaxError',
array('value' => $expanded_value));
}
self::addValue(
$reverse_map, $property, $item,
array('propertyIsArray' => true));
}
} }
continue;
} }
$container = self::getContextValue($active_ctx, $key, '@container'); // FIXME: can this be merged with code below to simplify?
// merge in all reversed properties
// handle language map container (skip if value is not an object) if(property_exists($rval, '@reverse')) {
if($container === '@language' && is_object($value)) { $reverse_map = $rval->{'@reverse'};
$expanded_value = $this->_expandLanguageMap($value);
}
// handle index container (skip if value is not an object)
else if($container === '@index' && is_object($value)) {
$expanded_value = array();
$value_keys = array_keys((array)$value);
sort($value_keys);
foreach($value_keys as $value_key) {
$val = $value->{$value_key};
$val = self::arrayify($val);
$val = $this->_expand($active_ctx, $key, $val, $options, false);
foreach($val as $item) {
if(!property_exists($item, '@index')) {
$item->{'@index'} = $value_key;
}
$expanded_value[] = $item;
}
}
} }
else { else {
// recurse into @list or @set keeping the active property $reverse_map = null;
$is_list = ($expanded_property === '@list'); }
if($is_list || $expanded_property === '@set') { foreach($expanded_value as $property => $items) {
$next_active_property = $active_property; if($property === '@reverse') {
if($is_list && $expanded_active_property === '@graph') { continue;
$next_active_property = null;
}
$expanded_value = $this->_expand(
$active_ctx, $next_active_property, $value, $options, $is_list);
if($is_list && self::_isList($expanded_value)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; lists of lists are not permitted.',
'jsonld.SyntaxError');
}
} }
else { if($reverse_map === null) {
// recursively expand value with key as new active property $reverse_map = $rval->{'@reverse'} = new stdClass();
$expanded_value = $this->_expand(
$active_ctx, $key, $value, $options, false);
} }
} self::addValue(
$reverse_map, $property, array(),
// drop null values if property is not @value array('propertyIsArray' => true));
if($expanded_value === null && $expanded_property !== '@value') { foreach($items as $item) {
continue;
}
// convert expanded value to @list if container specifies it
if($expanded_property !== '@list' && !self::_isList($expanded_value) &&
$container === '@list') {
// ensure expanded value is an array
$expanded_value = (object)array(
'@list' => self::arrayify($expanded_value));
}
// FIXME: can this be merged with code above to simplify?
// merge in reverse properties
if(property_exists($active_ctx->mappings, $key) &&
$active_ctx->mappings->{$key} &&
$active_ctx->mappings->{$key}->reverse) {
$reverse_map = $rval->{'@reverse'} = new stdClass();
$expanded_value = self::arrayify($expanded_value);
foreach($expanded_value as $item) {
if(self::_isValue($item) || self::_isList($item)) { if(self::_isValue($item) || self::_isList($item)) {
throw new JsonLdException( throw new JsonLdException(
'Invalid JSON-LD syntax; "@reverse" value must not be a ' + 'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
'@value or an @list.', '@value or an @list.',
'jsonld.SyntaxError', array('value' => $expanded_value)); 'jsonld.SyntaxError',
array('value' => $expanded_value));
} }
self::addValue( self::addValue(
$reverse_map, $expanded_property, $item, $reverse_map, $property, $item,
array('propertyIsArray' => true)); array('propertyIsArray' => true));
} }
continue;
} }
// add value for property continue;
// use an array except for certain keywords
$use_array = (!in_array(
$expanded_property, array(
'@index', '@id', '@type', '@value', '@language')));
self::addValue(
$rval, $expanded_property, $expanded_value,
array('propertyIsArray' => $use_array));
} }
// get property count on expanded output $container = self::getContextValue($active_ctx, $key, '@container');
$keys = array_keys((array)$rval);
$count = count($keys);
// @value must only have @language or @type // handle language map container (skip if value is not an object)
if(property_exists($rval, '@value')) { if($container === '@language' && is_object($value)) {
// @value must only have @language or @type $expanded_value = $this->_expandLanguageMap($value);
if(property_exists($rval, '@type') && }
property_exists($rval, '@language')) { // handle index container (skip if value is not an object)
throw new JsonLdException( else if($container === '@index' && is_object($value)) {
'Invalid JSON-LD syntax; an element containing "@value" may not ' . $expanded_value = array();
'contain both "@type" and "@language".', $value_keys = array_keys((array)$value);
'jsonld.SyntaxError', array('element' => $rval)); sort($value_keys);
} foreach($value_keys as $value_key) {
$valid_count = $count - 1; $val = $value->{$value_key};
if(property_exists($rval, '@type')) { $val = self::arrayify($val);
$valid_count -= 1; $val = $this->_expand($active_ctx, $key, $val, $options, false);
} foreach($val as $item) {
if(property_exists($rval, '@index')) { if(!property_exists($item, '@index')) {
$valid_count -= 1; $item->{'@index'} = $value_key;
} }
if(property_exists($rval, '@language')) { $expanded_value[] = $item;
$valid_count -= 1; }
}
if($valid_count !== 0) {
throw new JsonLdException(
'Invalid JSON-LD syntax; an element containing "@value" may only ' .
'have an "@index" property and at most one other property ' .
'which can be "@type" or "@language".',
'jsonld.SyntaxError', array('element' => $rval));
}
// drop null @values
if($rval->{'@value'} === null) {
$rval = null;
}
// drop @language if @value isn't a string
else if(property_exists($rval, '@language') &&
!is_string($rval->{'@value'})) {
unset($rval->{'@language'});
} }
} }
// convert @type to an array else {
else if(property_exists($rval, '@type') && !is_array($rval->{'@type'})) { // recurse into @list or @set keeping the active property
$rval->{'@type'} = array($rval->{'@type'}); $is_list = ($expanded_property === '@list');
} if($is_list || $expanded_property === '@set') {
// handle @set and @list $next_active_property = $active_property;
else if(property_exists($rval, '@set') || if($is_list && $expanded_active_property === '@graph') {
property_exists($rval, '@list')) { $next_active_property = null;
if($count > 1 && ($count !== 2 && property_exists($rval, '@index'))) { }
throw new JsonLdException( $expanded_value = $this->_expand(
'Invalid JSON-LD syntax; if an element has the property "@set" ' . $active_ctx, $next_active_property, $value, $options, $is_list);
'or "@list", then it can have at most one other property that is ' . if($is_list && self::_isList($expanded_value)) {
'"@index".', throw new JsonLdException(
'jsonld.SyntaxError', array('element' => $rval)); 'Invalid JSON-LD syntax; lists of lists are not permitted.',
} 'jsonld.SyntaxError');
// optimize away @set }
if(property_exists($rval, '@set')) {
$rval = $rval->{'@set'};
$keys = array_keys((array)$rval);
$count = count($keys);
}
}
// drop objects with only @language
else if($count === 1 && property_exists($rval, '@language')) {
$rval = null;
}
// drop certain top-level objects that do not occur in lists
if(is_object($rval) &&
!$options['keepFreeFloatingNodes'] && !$inside_list &&
($active_property === null || $expanded_active_property === '@graph')) {
// drop empty object or top-level @value
if($count === 0 || property_exists($rval, '@value')) {
$rval = null;
} }
else { else {
// drop subjects that generate no triples // recursively expand value with key as new active property
$has_triples = false; $expanded_value = $this->_expand(
$ignore = array('@graph', '@type'); $active_ctx, $key, $value, $options, false);
foreach($keys as $key) {
if(!self::_isKeyword($key) || in_array($key, $ignore)) {
$has_triples = true;
break;
}
}
if(!$has_triples) {
$rval = null;
}
} }
} }
return $rval; // drop null values if property is not @value
if($expanded_value === null && $expanded_property !== '@value') {
continue;
}
// convert expanded value to @list if container specifies it
if($expanded_property !== '@list' && !self::_isList($expanded_value) &&
$container === '@list') {
// ensure expanded value is an array
$expanded_value = (object)array(
'@list' => self::arrayify($expanded_value));
}
// FIXME: can this be merged with code above to simplify?
// merge in reverse properties
if(property_exists($active_ctx->mappings, $key) &&
$active_ctx->mappings->{$key} &&
$active_ctx->mappings->{$key}->reverse) {
$reverse_map = $rval->{'@reverse'} = new stdClass();
$expanded_value = self::arrayify($expanded_value);
foreach($expanded_value as $item) {
if(self::_isValue($item) || self::_isList($item)) {
throw new JsonLdException(
'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
'@value or an @list.',
'jsonld.SyntaxError', array('value' => $expanded_value));
}
self::addValue(
$reverse_map, $expanded_property, $item,
array('propertyIsArray' => true));
}
continue;
}
// add value for property
// use an array except for certain keywords
$use_array = (!in_array(
$expanded_property, array(
'@index', '@id', '@type', '@value', '@language')));
self::addValue(
$rval, $expanded_property, $expanded_value,
array('propertyIsArray' => $use_array));
} }
// drop top-level scalars that are not in lists // get property count on expanded output
if(!$inside_list && $keys = array_keys((array)$rval);
($active_property === null || $count = count($keys);
$this->_expandIri($active_ctx, $active_property,
array('vocab' => true)) === '@graph')) { // @value must only have @language or @type
return null; if(property_exists($rval, '@value')) {
// @value must only have @language or @type
if(property_exists($rval, '@type') &&
property_exists($rval, '@language')) {
throw new JsonLdException(
'Invalid JSON-LD syntax; an element containing "@value" may not ' .
'contain both "@type" and "@language".',
'jsonld.SyntaxError', array('element' => $rval));
}
$valid_count = $count - 1;
if(property_exists($rval, '@type')) {
$valid_count -= 1;
}
if(property_exists($rval, '@index')) {
$valid_count -= 1;
}
if(property_exists($rval, '@language')) {
$valid_count -= 1;
}
if($valid_count !== 0) {
throw new JsonLdException(
'Invalid JSON-LD syntax; an element containing "@value" may only ' .
'have an "@index" property and at most one other property ' .
'which can be "@type" or "@language".',
'jsonld.SyntaxError', array('element' => $rval));
}
// drop null @values
if($rval->{'@value'} === null) {
$rval = null;
}
// drop @language if @value isn't a string
else if(property_exists($rval, '@language') &&
!is_string($rval->{'@value'})) {
unset($rval->{'@language'});
}
}
// convert @type to an array
else if(property_exists($rval, '@type') && !is_array($rval->{'@type'})) {
$rval->{'@type'} = array($rval->{'@type'});
}
// handle @set and @list
else if(property_exists($rval, '@set') ||
property_exists($rval, '@list')) {
if($count > 1 && ($count !== 2 && property_exists($rval, '@index'))) {
throw new JsonLdException(
'Invalid JSON-LD syntax; if an element has the property "@set" ' .
'or "@list", then it can have at most one other property that is ' .
'"@index".',
'jsonld.SyntaxError', array('element' => $rval));
}
// optimize away @set
if(property_exists($rval, '@set')) {
$rval = $rval->{'@set'};
$keys = array_keys((array)$rval);
$count = count($keys);
}
}
// drop objects with only @language
else if($count === 1 && property_exists($rval, '@language')) {
$rval = null;
} }
// expand element according to value expansion rules // drop certain top-level objects that do not occur in lists
return $this->_expandValue($active_ctx, $active_property, $element); if(is_object($rval) &&
!$options['keepFreeFloatingNodes'] && !$inside_list &&
($active_property === null || $expanded_active_property === '@graph')) {
// drop empty object or top-level @value
if($count === 0 || property_exists($rval, '@value')) {
$rval = null;
}
else {
// drop subjects that generate no triples
$has_triples = false;
$ignore = array('@graph', '@type');
foreach($keys as $key) {
if(!self::_isKeyword($key) || in_array($key, $ignore)) {
$has_triples = true;
break;
}
}
if(!$has_triples) {
$rval = null;
}
}
}
return $rval;
} }
/** /**