package com.github.jsonldjava.core;
import com.github.jsonldjava.core.JsonLdError.Error;
import com.github.jsonldjava.utils.Obj;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.util.*;
import static com.github.jsonldjava.core.JsonLdConsts.*;
import static com.github.jsonldjava.core.JsonLdUtils.isKeyword;
import static com.github.jsonldjava.utils.Obj.newMap;
/**
* A container object to maintain state relating to JsonLdOptions and the
* current Context, and push these into the relevant algorithms in
* JsonLdProcessor as necessary.
*
* @author tristan
*/
public class JsonLdApi {
private final Logger log = LoggerFactory.getLogger(this.getClass());
JsonLdOptions opts;
Object value = null;
Context context = null;
private Map<String, String> blankNodeMapping = new HashMap<>();
/**
* Constructs an empty JsonLdApi object using the default JsonLdOptions, and
* without initialization.
*/
public JsonLdApi() {
this(new JsonLdOptions(""));
}
/**
* Constructs a JsonLdApi object using the given object as the initial
* JSON-LD object, and the given JsonLdOptions.
*
* @param input
* The initial JSON-LD object.
* @param opts
* The JsonLdOptions to use.
* @throws JsonLdError
* If there is an error initializing using the object and
* options.
*/
public JsonLdApi(Object input, JsonLdOptions opts) throws JsonLdError {
this(opts);
initialize(input, null);
}
/**
* Constructs a JsonLdApi object using the given object as the initial
* JSON-LD object, the given context, and the given JsonLdOptions.
*
* @param input
* The initial JSON-LD object.
* @param context
* The initial context.
* @param opts
* The JsonLdOptions to use.
* @throws JsonLdError
* If there is an error initializing using the object and
* options.
*/
public JsonLdApi(Object input, Object context, JsonLdOptions opts) throws JsonLdError {
this(opts);
initialize(input, null);
}
/**
* Constructs an empty JsonLdApi object using the given JsonLdOptions, and
* without initialization. <br>
* If the JsonLdOptions parameter is null, then the default options are
* used.
*
* @param opts
* The JsonLdOptions to use.
*/
public JsonLdApi(JsonLdOptions opts) {
if (opts == null) {
opts = new JsonLdOptions("");
} else {
this.opts = opts;
}
}
/**
* Initializes this object by cloning the input object using
* {@link JsonLdUtils#clone(Object)}, and by parsing the context using
* {@link Context#parse(Object)}.
*
* @param input
* The initial object, which is to be cloned and used in
* operations.
* @param context
* The context object, which is to be parsed and used in
* operations.
* @throws JsonLdError
* If there was an error cloning the object, or in parsing the
* context.
*/
private void initialize(Object input, Object context) throws JsonLdError {
if (input instanceof List || input instanceof Map) {
this.value = JsonLdUtils.clone(input);
}
// TODO: string/IO input
this.context = new Context(opts);
if (context != null) {
this.context = this.context.parse(context);
}
}
/***
* ____ _ _ _ _ _ _ / ___|___ _ __ ___ _ __ __ _ ___| |_ / \ | | __ _ ___ _
* __(_) |_| |__ _ __ ___ | | / _ \| '_ ` _ \| '_ \ / _` |/ __| __| / _ \ |
* |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ | |__| (_) | | | | | | |_) | (_|
* | (__| |_ / ___ \| | (_| | (_) | | | | |_| | | | | | | | | \____\___/|_|
* |_| |_| .__/ \__,_|\___|\__| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_|
* |_| |_| |___/
*/
/**
* Compaction Algorithm
*
* http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
*
* @param activeCtx
* The Active Context
* @param activeProperty
* The Active Property
* @param element
* The current element
* @param compactArrays
* True to compact arrays.
* @return The compacted JSON-LD object.
* @throws JsonLdError
* If there was an error during compaction.
*/
public Object compact(Context activeCtx, String activeProperty, Object element,
boolean compactArrays) throws JsonLdError {
// 2)
if (element instanceof List) {
// 2.1)
final List<Object> result = new ArrayList<>();
// 2.2)
for (final Object item : (List<Object>) element) {
// 2.2.1)
final Object compactedItem = compact(activeCtx, activeProperty, item, compactArrays);
// 2.2.2)
if (compactedItem != null) {
result.add(compactedItem);
}
}
// 2.3)
if (compactArrays && result.size() == 1
&& activeCtx.getContainer(activeProperty) == null) {
return result.get(0);
}
// 2.4)
return result;
}
// 3)
if (element instanceof Map) {
// access helper
final Map<String, Object> elem = (Map<String, Object>) element;
// 4
if (elem.containsKey("@value") || elem.containsKey("@id")) {
final Object compactedValue = activeCtx.compactValue(activeProperty, elem);
if (!(compactedValue instanceof Map || compactedValue instanceof List)) {
return compactedValue;
}
}
// 5)
final boolean insideReverse = "@reverse".equals(activeProperty);
// 6)
final Map<String, Object> result = newMap();
// 7)
final List<String> keys = new ArrayList<>(elem.keySet());
Collections.sort(keys);
for (final String expandedProperty : keys) {
final Object expandedValue = elem.get(expandedProperty);
// 7.1)
if ("@id".equals(expandedProperty) || "@type".equals(expandedProperty)) {
Object compactedValue;
// 7.1.1)
if (expandedValue instanceof String) {
compactedValue = activeCtx.compactIri((String) expandedValue,
"@type".equals(expandedProperty));
}
// 7.1.2)
else {
final List<String> types = new ArrayList<>();
// 7.1.2.2)
for (final String expandedType : (List<String>) expandedValue) {
types.add(activeCtx.compactIri(expandedType, true));
}
// 7.1.2.3)
if (types.size() == 1) {
compactedValue = types.get(0);
} else {
compactedValue = types;
}
}
// 7.1.3)
final String alias = activeCtx.compactIri(expandedProperty, true);
// 7.1.4)
result.put(alias, compactedValue);
continue;
// TODO: old add value code, see if it's still relevant?
// addValue(rval, alias, compactedValue,
// isArray(compactedValue)
// && ((List<Object>) expandedValue).size() == 0);
}
// 7.2)
if ("@reverse".equals(expandedProperty)) {
// 7.2.1)
final Map<String, Object> compactedValue = (Map<String, Object>) compact(
activeCtx, "@reverse", expandedValue, compactArrays);
// 7.2.2)
// Note: Must create a new set to avoid modifying the set we
// are iterating over
for (final String property : new HashSet<>(compactedValue.keySet())) {
final Object value = compactedValue.get(property);
// 7.2.2.1)
if (activeCtx.isReverseProperty(property)) {
// 7.2.2.1.1)
if (("@set".equals(activeCtx.getContainer(property)) || !compactArrays)
&& !(value instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(value);
result.put(property, tmp);
}
// 7.2.2.1.2)
if (!result.containsKey(property)) {
result.put(property, value);
}
// 7.2.2.1.3)
else {
if (!(result.get(property) instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(result.put(property, tmp));
}
if (value instanceof List) {
((List<Object>) result.get(property))
.addAll((List<Object>) value);
} else {
((List<Object>) result.get(property)).add(value);
}
}
// 7.2.2.1.4)
compactedValue.remove(property);
}
}
// 7.2.3)
if (!compactedValue.isEmpty()) {
// 7.2.3.1)
final String alias = activeCtx.compactIri("@reverse", true);
// 7.2.3.2)
result.put(alias, compactedValue);
}
// 7.2.4)
continue;
}
// 7.3)
if ("@index".equals(expandedProperty)
&& "@index".equals(activeCtx.getContainer(activeProperty))) {
continue;
}
// 7.4)
else if ("@index".equals(expandedProperty) || "@value".equals(expandedProperty)
|| "@language".equals(expandedProperty)) {
// 7.4.1)
final String alias = activeCtx.compactIri(expandedProperty, true);
// 7.4.2)
result.put(alias, expandedValue);
continue;
}
// NOTE: expanded value must be an array due to expansion
// algorithm.
// 7.5)
if (((List<Object>) expandedValue).isEmpty()) {
// 7.5.1)
final String itemActiveProperty = activeCtx.compactIri(expandedProperty,
expandedValue, true, insideReverse);
// 7.5.2)
if (!result.containsKey(itemActiveProperty)) {
result.put(itemActiveProperty, new ArrayList<>());
} else {
final Object value = result.get(itemActiveProperty);
if (!(value instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(value);
result.put(itemActiveProperty, tmp);
}
}
}
// 7.6)
for (final Object expandedItem : (List<Object>) expandedValue) {
// 7.6.1)
final String itemActiveProperty = activeCtx.compactIri(expandedProperty,
expandedItem, true, insideReverse);
// 7.6.2)
final String container = activeCtx.getContainer(itemActiveProperty);
// get @list value if appropriate
final boolean isList = (expandedItem instanceof Map && ((Map<String, Object>) expandedItem)
.containsKey("@list"));
Object list = null;
if (isList) {
list = ((Map<String, Object>) expandedItem).get("@list");
}
// 7.6.3)
Object compactedItem = compact(activeCtx, itemActiveProperty, isList ? list
: expandedItem, compactArrays);
// 7.6.4)
if (isList) {
// 7.6.4.1)
if (!(compactedItem instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(compactedItem);
compactedItem = tmp;
}
// 7.6.4.2)
if (!"@list".equals(container)) {
// 7.6.4.2.1)
final Map<String, Object> wrapper = newMap();
// TODO: SPEC: no mention of vocab = true
wrapper.put(activeCtx.compactIri("@list", true), compactedItem);
compactedItem = wrapper;
// 7.6.4.2.2)
if (((Map<String, Object>) expandedItem).containsKey("@index")) {
((Map<String, Object>) compactedItem).put(
// TODO: SPEC: no mention of vocab =
// true
activeCtx.compactIri("@index", true),
((Map<String, Object>) expandedItem).get("@index"));
}
}
// 7.6.4.3)
else if (result.containsKey(itemActiveProperty)) {
throw new JsonLdError(Error.COMPACTION_TO_LIST_OF_LISTS,
"There cannot be two list objects associated with an active property that has a container mapping");
}
}
// 7.6.5)
if ("@language".equals(container) || "@index".equals(container)) {
// 7.6.5.1)
Map<String, Object> mapObject;
if (result.containsKey(itemActiveProperty)) {
mapObject = (Map<String, Object>) result.get(itemActiveProperty);
} else {
mapObject = newMap();
result.put(itemActiveProperty, mapObject);
}
// 7.6.5.2)
if ("@language".equals(container)
&& (compactedItem instanceof Map && ((Map<String, Object>) compactedItem)
.containsKey("@value"))) {
compactedItem = ((Map<String, Object>) compactedItem).get("@value");
}
// 7.6.5.3)
final String mapKey = (String) ((Map<String, Object>) expandedItem)
.get(container);
// 7.6.5.4)
if (!mapObject.containsKey(mapKey)) {
mapObject.put(mapKey, compactedItem);
} else {
List<Object> tmp;
if (!(mapObject.get(mapKey) instanceof List)) {
tmp = new ArrayList<>();
tmp.add(mapObject.put(mapKey, tmp));
} else {
tmp = (List<Object>) mapObject.get(mapKey);
}
tmp.add(compactedItem);
}
}
// 7.6.6)
else {
// 7.6.6.1)
final Boolean check = (!compactArrays || "@set".equals(container)
|| "@list".equals(container) || "@list".equals(expandedProperty) || "@graph"
.equals(expandedProperty))
&& (!(compactedItem instanceof List));
if (check) {
final List<Object> tmp = new ArrayList<>();
tmp.add(compactedItem);
compactedItem = tmp;
}
// 7.6.6.2)
if (!result.containsKey(itemActiveProperty)) {
result.put(itemActiveProperty, compactedItem);
} else {
if (!(result.get(itemActiveProperty) instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(result.put(itemActiveProperty, tmp));
}
if (compactedItem instanceof List) {
((List<Object>) result.get(itemActiveProperty))
.addAll((List<Object>) compactedItem);
} else {
((List<Object>) result.get(itemActiveProperty)).add(compactedItem);
}
}
}
}
}
// 8)
return result;
}
// 2)
return element;
}
/**
* Compaction Algorithm
*
* http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
*
* @param activeCtx
* The Active Context
* @param activeProperty
* The Active Property
* @param element
* The current element
* @return The compacted JSON-LD object.
* @throws JsonLdError
* If there was an error during compaction.
*/
public Object compact(Context activeCtx, String activeProperty, Object element)
throws JsonLdError {
return compact(activeCtx, activeProperty, element, true);
}
/***
* _____ _ _ _ _ _ _ | ____|_ ___ __ __ _ _ __ __| | / \ | | __ _ ___ _
* __(_) |_| |__ _ __ ___ | _| \ \/ / '_ \ / _` | '_ \ / _` | / _ \ | |/ _`
* |/ _ \| '__| | __| '_ \| '_ ` _ \ | |___ > <| |_) | (_| | | | | (_| | /
* ___ \| | (_| | (_) | | | | |_| | | | | | | | | |_____/_/\_\ .__/ \__,_|_|
* |_|\__,_| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| |_| |___/
*/
/**
* Expansion Algorithm
*
* http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
*
* @param activeCtx
* The Active Context
* @param activeProperty
* The Active Property
* @param element
* The current element
* @return The expanded JSON-LD object.
* @throws JsonLdError
* If there was an error during expansion.
*/
public Object expand(Context activeCtx, String activeProperty, Object element)
throws JsonLdError {
// 1)
if (element == null) {
return null;
}
// 3)
if (element instanceof List) {
// 3.1)
final List<Object> result = new ArrayList<>();
// 3.2)
for (final Object item : (List<Object>) element) {
// 3.2.1)
final Object v = expand(activeCtx, activeProperty, item);
// 3.2.2)
if (("@list".equals(activeProperty) || "@list".equals(activeCtx
.getContainer(activeProperty)))
&& (v instanceof List || (v instanceof Map && ((Map<String, Object>) v)
.containsKey("@list")))) {
throw new JsonLdError(Error.LIST_OF_LISTS, "lists of lists are not permitted.");
}
// 3.2.3)
else if (v != null) {
if (v instanceof List) {
result.addAll((Collection<? extends Object>) v);
} else {
result.add(v);
}
}
}
// 3.3)
return result;
}
// 4)
else if (element instanceof Map) {
// access helper
final Map<String, Object> elem = (Map<String, Object>) element;
// 5)
if (elem.containsKey("@context")) {
activeCtx = activeCtx.parse(elem.get("@context"));
}
// 6)
Map<String, Object> result = newMap();
// 7)
final List<String> keys = new ArrayList<>(elem.keySet());
Collections.sort(keys);
for (final String key : keys) {
final Object value = elem.get(key);
// 7.1)
if (key.equals("@context")) {
continue;
}
// 7.2)
final String expandedProperty = activeCtx.expandIri(key, false, true, null, null);
Object expandedValue = null;
// 7.3)
if (expandedProperty == null
|| (!expandedProperty.contains(":") && !isKeyword(expandedProperty))) {
continue;
}
// 7.4)
if (isKeyword(expandedProperty)) {
// 7.4.1)
if ("@reverse".equals(activeProperty)) {
throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_MAP,
"a keyword cannot be used as a @reverse propery");
}
// 7.4.2)
if (result.containsKey(expandedProperty)) {
throw new JsonLdError(Error.COLLIDING_KEYWORDS, expandedProperty
+ " already exists in result");
}
// 7.4.3)
if ("@id".equals(expandedProperty)) {
if (!(value instanceof String)) {
throw new JsonLdError(Error.INVALID_ID_VALUE,
"value of @id must be a string");
}
expandedValue = activeCtx
.expandIri((String) value, true, false, null, null);
}
// 7.4.4)
else if ("@type".equals(expandedProperty)) {
if (value instanceof List) {
expandedValue = new ArrayList<String>();
for (final Object v : (List) value) {
if (!(v instanceof String)) {
throw new JsonLdError(Error.INVALID_TYPE_VALUE,
"@type value must be a string or array of strings");
}
((List<String>) expandedValue).add(activeCtx.expandIri((String) v,
true, true, null, null));
}
} else if (value instanceof String) {
expandedValue = activeCtx.expandIri((String) value, true, true, null,
null);
}
// TODO: SPEC: no mention of empty map check
else if (value instanceof Map) {
if (!((Map<String, Object>) value).isEmpty()) {
throw new JsonLdError(Error.INVALID_TYPE_VALUE,
"@type value must be a an empty object for framing");
}
expandedValue = value;
} else {
throw new JsonLdError(Error.INVALID_TYPE_VALUE,
"@type value must be a string or array of strings");
}
}
// 7.4.5)
else if ("@graph".equals(expandedProperty)) {
expandedValue = expand(activeCtx, "@graph", value);
}
// 7.4.6)
else if ("@value".equals(expandedProperty)) {
if (value != null && (value instanceof Map || value instanceof List)) {
throw new JsonLdError(Error.INVALID_VALUE_OBJECT_VALUE, "value of "
+ expandedProperty + " must be a scalar or null");
}
expandedValue = value;
if (expandedValue == null) {
result.put("@value", null);
continue;
}
}
// 7.4.7)
else if ("@language".equals(expandedProperty)) {
if (!(value instanceof String)) {
throw new JsonLdError(Error.INVALID_LANGUAGE_TAGGED_STRING, "Value of "
+ expandedProperty + " must be a string");
}
expandedValue = ((String) value).toLowerCase();
}
// 7.4.8)
else if ("@index".equals(expandedProperty)) {
if (!(value instanceof String)) {
throw new JsonLdError(Error.INVALID_INDEX_VALUE, "Value of "
+ expandedProperty + " must be a string");
}
expandedValue = value;
}
// 7.4.9)
else if ("@list".equals(expandedProperty)) {
// 7.4.9.1)
if (activeProperty == null || "@graph".equals(activeProperty)) {
continue;
}
// 7.4.9.2)
expandedValue = expand(activeCtx, activeProperty, value);
// NOTE: step not in the spec yet
if (!(expandedValue instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(expandedValue);
expandedValue = tmp;
}
// 7.4.9.3)
for (final Object o : (List<Object>) expandedValue) {
if (o instanceof Map && ((Map<String, Object>) o).containsKey("@list")) {
throw new JsonLdError(Error.LIST_OF_LISTS,
"A list may not contain another list");
}
}
}
// 7.4.10)
else if ("@set".equals(expandedProperty)) {
expandedValue = expand(activeCtx, activeProperty, value);
}
// 7.4.11)
else if ("@reverse".equals(expandedProperty)) {
if (!(value instanceof Map)) {
throw new JsonLdError(Error.INVALID_REVERSE_VALUE,
"@reverse value must be an object");
}
// 7.4.11.1)
expandedValue = expand(activeCtx, "@reverse", value);
// NOTE: algorithm assumes the result is a map
// 7.4.11.2)
if (((Map<String, Object>) expandedValue).containsKey("@reverse")) {
final Map<String, Object> reverse = (Map<String, Object>) ((Map<String, Object>) expandedValue)
.get("@reverse");
for (final Map.Entry<String, Object> stringObjectEntry : reverse.entrySet()) {
final Object item = stringObjectEntry.getValue();
// 7.4.11.2.1)
if (!result.containsKey(stringObjectEntry.getKey())) {
result.put(stringObjectEntry.getKey(), new ArrayList<>());
}
// 7.4.11.2.2)
if (item instanceof List) {
((List<Object>) result.get(stringObjectEntry.getKey()))
.addAll((List<Object>) item);
} else {
((List<Object>) result.get(stringObjectEntry.getKey())).add(item);
}
}
}
// 7.4.11.3)
if (((Map<String, Object>) expandedValue).size() > (((Map<String, Object>) expandedValue)
.containsKey("@reverse") ? 1 : 0)) {
// 7.4.11.3.1)
if (!result.containsKey("@reverse")) {
result.put("@reverse", newMap());
}
// 7.4.11.3.2)
final Map<String, Object> reverseMap = (Map<String, Object>) result
.get("@reverse");
// 7.4.11.3.3)
for (final String property : ((Map<String, Object>) expandedValue)
.keySet()) {
if ("@reverse".equals(property)) {
continue;
}
// 7.4.11.3.3.1)
final List<Object> items = (List<Object>) ((Map<String, Object>) expandedValue)
.get(property);
for (final Object item : items) {
// 7.4.11.3.3.1.1)
if (item instanceof Map
&& (((Map<String, Object>) item).containsKey("@value") || ((Map<String, Object>) item)
.containsKey("@list"))) {
throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_VALUE);
}
// 7.4.11.3.3.1.2)
if (!reverseMap.containsKey(property)) {
reverseMap.put(property, new ArrayList<>());
}
// 7.4.11.3.3.1.3)
((List<Object>) reverseMap.get(property)).add(item);
}
}
}
// 7.4.11.4)
continue;
}
// TODO: SPEC no mention of @explicit etc in spec
else if ("@explicit".equals(expandedProperty)
|| "@default".equals(expandedProperty)
|| "@embed".equals(expandedProperty)
|| "@embedChildren".equals(expandedProperty)
|| "@omitDefault".equals(expandedProperty)) {
expandedValue = expand(activeCtx, expandedProperty, value);
}
// 7.4.12)
if (expandedValue != null) {
result.put(expandedProperty, expandedValue);
}
// 7.4.13)
continue;
}
// 7.5
else if ("@language".equals(activeCtx.getContainer(key)) && value instanceof Map) {
// 7.5.1)
expandedValue = new ArrayList<>();
// 7.5.2)
for (final String language : ((Map<String, Object>) value).keySet()) {
Object languageValue = ((Map<String, Object>) value).get(language);
// 7.5.2.1)
if (!(languageValue instanceof List)) {
final Object tmp = languageValue;
languageValue = new ArrayList<>();
((List<Object>) languageValue).add(tmp);
}
// 7.5.2.2)
for (final Object item : (List<Object>) languageValue) {
// 7.5.2.2.1)
if (!(item instanceof String)) {
throw new JsonLdError(Error.INVALID_LANGUAGE_MAP_VALUE, "Expected "
+ item.toString() + " to be a string");
}
// 7.5.2.2.2)
final Map<String, Object> tmp = newMap();
tmp.put("@value", item);
tmp.put("@language", language.toLowerCase());
((List<Object>) expandedValue).add(tmp);
}
}
}
// 7.6)
else if ("@index".equals(activeCtx.getContainer(key)) && value instanceof Map) {
// 7.6.1)
expandedValue = new ArrayList<>();
// 7.6.2)
final List<String> indexKeys = new ArrayList<>(
((Map<String, Object>) value).keySet());
Collections.sort(indexKeys);
for (final String index : indexKeys) {
Object indexValue = ((Map<String, Object>) value).get(index);
// 7.6.2.1)
if (!(indexValue instanceof List)) {
final Object tmp = indexValue;
indexValue = new ArrayList<>();
((List<Object>) indexValue).add(tmp);
}
// 7.6.2.2)
indexValue = expand(activeCtx, key, indexValue);
// 7.6.2.3)
for (final Map<String, Object> item : (List<Map<String, Object>>) indexValue) {
// 7.6.2.3.1)
if (!item.containsKey("@index")) {
item.put("@index", index);
}
// 7.6.2.3.2)
((List<Object>) expandedValue).add(item);
}
}
}
// 7.7)
else {
expandedValue = expand(activeCtx, key, value);
}
// 7.8)
if (expandedValue == null) {
continue;
}
// 7.9)
if ("@list".equals(activeCtx.getContainer(key))) {
if (!(expandedValue instanceof Map)
|| !((Map<String, Object>) expandedValue).containsKey("@list")) {
Object tmp = expandedValue;
if (!(tmp instanceof List)) {
tmp = new ArrayList<>();
((List<Object>) tmp).add(expandedValue);
}
expandedValue = newMap();
((Map<String, Object>) expandedValue).put("@list", tmp);
}
}
// 7.10)
if (activeCtx.isReverseProperty(key)) {
// 7.10.1)
if (!result.containsKey("@reverse")) {
result.put("@reverse", newMap());
}
// 7.10.2)
final Map<String, Object> reverseMap = (Map<String, Object>) result
.get("@reverse");
// 7.10.3)
if (!(expandedValue instanceof List)) {
final Object tmp = expandedValue;
expandedValue = new ArrayList<>();
((List<Object>) expandedValue).add(tmp);
}
// 7.10.4)
for (final Object item : (List<Object>) expandedValue) {
// 7.10.4.1)
if (item instanceof Map
&& (((Map<String, Object>) item).containsKey("@value") || ((Map<String, Object>) item)
.containsKey("@list"))) {
throw new JsonLdError(Error.INVALID_REVERSE_PROPERTY_VALUE);
}
// 7.10.4.2)
if (!reverseMap.containsKey(expandedProperty)) {
reverseMap.put(expandedProperty, new ArrayList<>());
}
// 7.10.4.3)
if (item instanceof List) {
((List<Object>) reverseMap.get(expandedProperty))
.addAll((List<Object>) item);
} else {
((List<Object>) reverseMap.get(expandedProperty)).add(item);
}
}
}
// 7.11)
else {
// 7.11.1)
if (!result.containsKey(expandedProperty)) {
result.put(expandedProperty, new ArrayList<>());
}
// 7.11.2)
if (expandedValue instanceof List) {
((List<Object>) result.get(expandedProperty))
.addAll((List<Object>) expandedValue);
} else {
((List<Object>) result.get(expandedProperty)).add(expandedValue);
}
}
}
// 8)
if (result.containsKey("@value")) {
// 8.1)
// TODO: is this method faster than just using containsKey for
// each?
final Set<String> keySet = new HashSet(result.keySet());
keySet.remove("@value");
keySet.remove("@index");
final boolean langremoved = keySet.remove("@language");
final boolean typeremoved = keySet.remove("@type");
if ((langremoved && typeremoved) || !keySet.isEmpty()) {
throw new JsonLdError(Error.INVALID_VALUE_OBJECT,
"value object has unknown keys");
}
// 8.2)
final Object rval = result.get("@value");
if (rval == null) {
// nothing else is possible with result if we set it to
// null, so simply return it
return null;
}
// 8.3)
if (!(rval instanceof String) && result.containsKey("@language")) {
throw new JsonLdError(Error.INVALID_LANGUAGE_TAGGED_VALUE,
"when @language is used, @value must be a string");
}
// 8.4)
else if (result.containsKey("@type")) {
// TODO: is this enough for "is an IRI"
if (!(result.get("@type") instanceof String)
|| ((String) result.get("@type")).startsWith("_:")
|| !((String) result.get("@type")).contains(":")) {
throw new JsonLdError(Error.INVALID_TYPED_VALUE,
"value of @type must be an IRI");
}
}
}
// 9)
else if (result.containsKey("@type")) {
final Object rtype = result.get("@type");
if (!(rtype instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(rtype);
result.put("@type", tmp);
}
}
// 10)
else if (result.containsKey("@set") || result.containsKey("@list")) {
// 10.1)
if (result.size() > (result.containsKey("@index") ? 2 : 1)) {
throw new JsonLdError(Error.INVALID_SET_OR_LIST_OBJECT,
"@set or @list may only contain @index");
}
// 10.2)
if (result.containsKey("@set")) {
// result becomes an array here, thus the remaining checks
// will never be true from here on
// so simply return the value rather than have to make
// result an object and cast it with every
// other use in the function.
return result.get("@set");
}
}
// 11)
if (result.containsKey("@language") && result.size() == 1) {
result = null;
}
// 12)
if (activeProperty == null || "@graph".equals(activeProperty)) {
// 12.1)
if (result != null
&& (result.isEmpty() || result.containsKey("@value") || result
.containsKey("@list"))) {
result = null;
}
// 12.2)
else if (result != null && result.containsKey("@id") && result.size() == 1) {
result = null;
}
}
// 13)
return result;
}
// 2) If element is a scalar
else {
// 2.1)
if (activeProperty == null || "@graph".equals(activeProperty)) {
return null;
}
return activeCtx.expandValue(activeProperty, element);
}
}
/**
* Expansion Algorithm
*
* http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
*
* @param activeCtx
* The Active Context
* @param element
* The current element
* @return The expanded JSON-LD object.
* @throws JsonLdError
* If there was an error during expansion.
*/
public Object expand(Context activeCtx, Object element) throws JsonLdError {
return expand(activeCtx, null, element);
}
/***
* _____ _ _ _ _ _ _ _ _ | ___| | __ _| |_| |_ ___ _ __ / \ | | __ _ ___ _
* __(_) |_| |__ _ __ ___ | |_ | |/ _` | __| __/ _ \ '_ \ / _ \ | |/ _` |/ _
* \| '__| | __| '_ \| '_ ` _ \ | _| | | (_| | |_| || __/ | | | / ___ \| |
* (_| | (_) | | | | |_| | | | | | | | | |_| |_|\__,_|\__|\__\___|_| |_| /_/
* \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| |___/
*/
void generateNodeMap(Object element, Map<String, Object> nodeMap) throws JsonLdError {
generateNodeMap(element, nodeMap, "@default", null, null, null);
}
void generateNodeMap(Object element, Map<String, Object> nodeMap, String activeGraph)
throws JsonLdError {
generateNodeMap(element, nodeMap, activeGraph, null, null, null);
}
void generateNodeMap(Object element, Map<String, Object> nodeMap, String activeGraph,
Object activeSubject, String activeProperty, Map<String, Object> list)
throws JsonLdError {
// 1)
if (element instanceof List) {
// 1.1)
for (final Object item : (List<Object>) element) {
generateNodeMap(item, nodeMap, activeGraph, activeSubject, activeProperty, list);
}
return;
}
// for convenience
final Map<String, Object> elem = (Map<String, Object>) element;
// 2)
if (!nodeMap.containsKey(activeGraph)) {
nodeMap.put(activeGraph, newMap());
}
final Map<String, Object> graph = (Map<String, Object>) nodeMap.get(activeGraph);
Map<String, Object> node = (Map<String, Object>) (activeSubject == null ? null : graph
.get(activeSubject));
if (activeSubject != null && activeSubject.toString().startsWith("_:")) {
String old = blankNodeMapping.get(activeSubject.toString());
blankNodeMapping.put(activeSubject.toString(), old + generateBlankNodeId(activeProperty, element));
}
// 3)
if (elem.containsKey("@type")) {
// 3.1)
List<String> oldTypes;
final List<String> newTypes = new ArrayList<>();
if (elem.get("@type") instanceof List) {
oldTypes = (List<String>) elem.get("@type");
} else {
oldTypes = new ArrayList<>();
oldTypes.add((String) elem.get("@type"));
}
for (final String item : oldTypes) {
if (item.startsWith("_:")) {
newTypes.add(generateBlankNodeIdentifier(item));
} else {
newTypes.add(item);
}
}
if (elem.get("@type") instanceof List) {
elem.put("@type", newTypes);
} else {
elem.put("@type", newTypes.get(0));
}
}
// 4)
if (elem.containsKey("@value")) {
// 4.1)
if (list == null) {
JsonLdUtils.mergeValue(node, activeProperty, elem);
}
// 4.2)
else {
JsonLdUtils.mergeValue(list, "@list", elem);
}
}
// 5)
else if (elem.containsKey("@list")) {
// 5.1)
final Map<String, Object> result = newMap("@list", new ArrayList<>());
// 5.2)
// for (final Object item : (List<Object>) elem.get("@list")) {
// generateNodeMap(item, nodeMap, activeGraph, activeSubject,
// activeProperty, result);
// }
generateNodeMap(elem.get("@list"), nodeMap, activeGraph, activeSubject, activeProperty,
result);
// 5.3)
JsonLdUtils.mergeValue(node, activeProperty, result);
}
// 6)
else {
// 6.1)
String id = (String) elem.remove("@id");
if (id != null) {
if (id.startsWith("_:")) {
id = generateBlankNodeIdentifier(id);
}
}
// 6.2)
else {
id = generateBlankNodeIdentifier(null);
}
// 6.3)
if (!graph.containsKey(id)) {
final Map<String, Object> tmp = newMap("@id", id);
graph.put(id, tmp);
if (id.startsWith("_:")) {
blankNodeMapping.put(id, id);
}
}
// 6.4) TODO: SPEC this line is asked for by the spec, but it breaks
// various tests
// node = (Map<String, Object>) graph.get(id);
// 6.5)
if (activeSubject instanceof Map) {
// 6.5.1)
JsonLdUtils.mergeValue((Map<String, Object>) graph.get(id), activeProperty,
activeSubject);
}
// 6.6)
else if (activeProperty != null) {
final Map<String, Object> reference = newMap("@id", id);
// 6.6.2)
if (list == null) {
// 6.6.2.1+2)
JsonLdUtils.mergeValue(node, activeProperty, reference);
}
// 6.6.3) TODO: SPEC says to add ELEMENT to @list member, should
// be REFERENCE
else {
JsonLdUtils.mergeValue(list, "@list", reference);
}
}
// TODO: SPEC this is removed in the spec now, but it's still needed
// (see 6.4)
node = (Map<String, Object>) graph.get(id);
// 6.7)
if (elem.containsKey("@type")) {
for (final Object type : (List<Object>) elem.remove("@type")) {
JsonLdUtils.mergeValue(node, "@type", type);
}
}
// 6.8)
if (elem.containsKey("@index")) {
final Object elemIndex = elem.remove("@index");
if (node.containsKey("@index")) {
if (!JsonLdUtils.deepCompare(node.get("@index"), elemIndex)) {
throw new JsonLdError(Error.CONFLICTING_INDEXES);
}
} else {
node.put("@index", elemIndex);
}
}
// 6.9)
if (elem.containsKey("@reverse")) {
// 6.9.1)
final Map<String, Object> referencedNode = newMap("@id", id);
// 6.9.2+6.9.4)
final Map<String, Object> reverseMap = (Map<String, Object>) elem
.remove("@reverse");
// 6.9.3)
for (final Map.Entry<String, Object> stringObjectEntry : reverseMap.entrySet()) {
final List<Object> values = (List<Object>) stringObjectEntry.getValue();
// 6.9.3.1)
for (final Object value : values) {
// 6.9.3.1.1)
generateNodeMap(value, nodeMap, activeGraph, referencedNode, stringObjectEntry.getKey(), null);
}
}
}
// 6.10)
if (elem.containsKey("@graph")) {
generateNodeMap(elem.remove("@graph"), nodeMap, id, null, null, null);
}
// 6.11)
final List<String> keys = new ArrayList<>(elem.keySet());
Collections.sort(keys);
for (String property : keys) {
final Object value = elem.get(property);
// 6.11.1)
if (property.startsWith("_:")) {
property = generateBlankNodeIdentifier(property);
}
// 6.11.2)
if (!node.containsKey(property)) {
node.put(property, new ArrayList<>());
}
// 6.11.3)
generateNodeMap(value, nodeMap, activeGraph, id, property, null);
}
}
}
private String generateBlankNodeId(String property, Object elem) {
StringBuilder builder = new StringBuilder();
builder.append("_").append(DigestUtils.sha256Hex(property));
if (elem instanceof Map) {
Map element = (Map) elem;
Object atValue = element.get("@value");
if (atValue != null) {
builder.append("_").append(DigestUtils.sha256Hex(atValue.toString()));
}
}
return builder.toString();
}
/**
* Blank Node identifier map specified in:
*
* http://www.w3.org/TR/json-ld-api/#generate-blank-node-identifier
*/
private final Map<String, String> blankNodeIdentifierMap = new LinkedHashMap<>();
/**
* Generates a blank node identifier for the given key using the algorithm
* specified in:
*
* http://www.w3.org/TR/json-ld-api/#generate-blank-node-identifier
*
* @param id
* The id, or null to generate a fresh, unused, blank node
* identifier.
* @return A blank node identifier based on id if it was not null, or a
* fresh, unused, blank node identifier if it was null.
*/
String generateBlankNodeIdentifier(String id) {
if (id != null && blankNodeIdentifierMap.containsKey(id)) {
return blankNodeIdentifierMap.get(id);
}
final String bnid = "_:b" + UUID.randomUUID();
if (id != null) {
blankNodeIdentifierMap.put(id, bnid);
}
return bnid;
}
/**
* Generates a fresh, unused, blank node identifier using the algorithm
* specified in:
*
* http://www.w3.org/TR/json-ld-api/#generate-blank-node-identifier
*
* @return A fresh, unused, blank node identifier.
*/
String generateBlankNodeIdentifier() {
return generateBlankNodeIdentifier(null);
}
/***
* _____ _ _ _ _ _ _ | ___| __ __ _ _ __ ___ (_)_ __ __ _ / \ | | __ _ ___ _
* __(_) |_| |__ _ __ ___ | |_ | '__/ _` | '_ ` _ \| | '_ \ / _` | / _ \ |
* |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ | _|| | | (_| | | | | | | | | | |
* (_| | / ___ \| | (_| | (_) | | | | |_| | | | | | | | | |_| |_| \__,_|_|
* |_| |_|_|_| |_|\__, | /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_|
* |___/ |___/
*/
private class FramingContext {
public boolean embed;
public boolean explicit;
public boolean omitDefault;
public FramingContext() {
embed = true;
explicit = false;
omitDefault = false;
embeds = null;
}
public FramingContext(JsonLdOptions opts) {
this();
if (opts.getEmbed() != null) {
this.embed = opts.getEmbed();
}
if (opts.getExplicit() != null) {
this.explicit = opts.getExplicit();
}
if (opts.getOmitDefault() != null) {
this.omitDefault = opts.getOmitDefault();
}
}
public Map<String, EmbedNode> embeds = null;
}
private class EmbedNode {
public Object parent = null;
public String property = null;
}
private Map<String, Object> nodeMap;
/**
* Performs JSON-LD <a
* href="http://json-ld.org/spec/latest/json-ld-framing/">framing</a>.
*
* @param input
* the expanded JSON-LD to frame.
* @param frame
* the expanded JSON-LD frame to use.
* @return the framed output.
* @throws JsonLdError
* If the framing was not successful.
*/
public List<Object> frame(Object input, List<Object> frame) throws JsonLdError {
// create framing state
final FramingContext state = new FramingContext(this.opts);
// use tree map so keys are sotred by default
final Map<String, Object> nodes = new TreeMap<>();
generateNodeMap(input, nodes);
this.nodeMap = (Map<String, Object>) nodes.get("@default");
final List<Object> framed = new ArrayList<>();
// NOTE: frame validation is done by the function not allowing anything
// other than list to me passed
frame(state,
this.nodeMap,
(frame != null && !frame.isEmpty() ? (Map<String, Object>) frame.get(0) : newMap()),
framed, null);
return framed;
}
/**
* Frames subjects according to the given frame.
*
* @param state
* the current framing state.
* @param subjects
* the subjects to filter.
* @param frame
* the frame.
* @param parent
* the parent subject or top-level array.
* @param property
* the parent property, initialized to null.
* @throws JsonLdError
* If there was an error during framing.
*/
private void frame(FramingContext state, Map<String, Object> nodes, Map<String, Object> frame,
Object parent, String property) throws JsonLdError {
// filter out subjects that match the frame
final Map<String, Object> matches = filterNodes(state, nodes, frame);
// get flags for current frame
Boolean embedOn = getFrameFlag(frame, "@embed", state.embed);
final Boolean explicicOn = getFrameFlag(frame, "@explicit", state.explicit);
// add matches to output
final List<String> ids = new ArrayList<>(matches.keySet());
Collections.sort(ids);
for (final String id : ids) {
if (property == null) {
state.embeds = new LinkedHashMap<>();
}
// start output
final Map<String, Object> output = newMap();
output.put("@id", id);
// prepare embed meta info
final EmbedNode embeddedNode = new EmbedNode();
embeddedNode.parent = parent;
embeddedNode.property = property;
// if embed is on and there is an existing embed
if (embedOn && state.embeds.containsKey(id)) {
final EmbedNode existing = state.embeds.get(id);
embedOn = false;
if (existing.parent instanceof List) {
for (final Object p : (List<Object>) existing.parent) {
if (JsonLdUtils.compareValues(output, p)) {
embedOn = true;
break;
}
}
}
// existing embed's parent is an object
else {
if (((Map<String, Object>) existing.parent).containsKey(existing.property)) {
for (final Object v : (List<Object>) ((Map<String, Object>) existing.parent)
.get(existing.property)) {
if (v instanceof Map
&& Obj.equals(id, ((Map<String, Object>) v).get("@id"))) {
embedOn = true;
break;
}
}
}
}
// existing embed has already been added, so allow an overwrite
if (embedOn) {
removeEmbed(state, id);
}
}
// not embedding, add output without any other properties
if (!embedOn) {
addFrameOutput(state, parent, property, output);
} else {
// add embed meta info
state.embeds.put(id, embeddedNode);
// iterate over subject properties
final Map<String, Object> element = (Map<String, Object>) matches.get(id);
List<String> props = new ArrayList<>(element.keySet());
Collections.sort(props);
for (final String prop : props) {
// copy keywords to output
if (isKeyword(prop)) {
output.put(prop, JsonLdUtils.clone(element.get(prop)));
continue;
}
// if property isn't in the frame
if (!frame.containsKey(prop)) {
// if explicit is off, embed values
if (!explicicOn) {
embedValues(state, element, prop, output);
}
continue;
}
// add objects
final List<Object> value = (List<Object>) element.get(prop);
for (final Object item : value) {
// recurse into list
if ((item instanceof Map)
&& ((Map<String, Object>) item).containsKey("@list")) {
// add empty list
final Map<String, Object> list = newMap();
list.put("@list", new ArrayList<>());
addFrameOutput(state, output, prop, list);
// add list objects
for (final Object listitem : (List<Object>) ((Map<String, Object>) item)
.get("@list")) {
// recurse into subject reference
if (JsonLdUtils.isNodeReference(listitem)) {
final Map<String, Object> tmp = newMap();
final String itemid = (String) ((Map<String, Object>) listitem)
.get("@id");
// TODO: nodes may need to be node_map,
// which is global
tmp.put(itemid, this.nodeMap.get(itemid));
frame(state, tmp,
(Map<String, Object>) ((List<Object>) frame.get(prop))
.get(0), list, "@list");
} else {
// include other values automatcially (TODO:
// may need JsonLdUtils.clone(n))
addFrameOutput(state, list, "@list", listitem);
}
}
}
// recurse into subject reference
else if (JsonLdUtils.isNodeReference(item)) {
final Map<String, Object> tmp = newMap();
final String itemid = (String) ((Map<String, Object>) item).get("@id");
// TODO: nodes may need to be node_map, which is
// global
tmp.put(itemid, this.nodeMap.get(itemid));
frame(state, tmp,
(Map<String, Object>) ((List<Object>) frame.get(prop)).get(0),
output, prop);
} else {
// include other values automatically (TODO: may
// need JsonLdUtils.clone(o))
addFrameOutput(state, output, prop, item);
}
}
}
// handle defaults
props = new ArrayList<>(frame.keySet());
Collections.sort(props);
for (final String prop : props) {
// skip keywords
if (isKeyword(prop)) {
continue;
}
final List<Object> pf = (List<Object>) frame.get(prop);
Map<String, Object> propertyFrame = !pf.isEmpty() ? (Map<String, Object>) pf
.get(0) : null;
if (propertyFrame == null) {
propertyFrame = newMap();
}
final boolean omitDefaultOn = getFrameFlag(propertyFrame, "@omitDefault",
state.omitDefault);
if (!omitDefaultOn && !output.containsKey(prop)) {
Object def = "@null";
if (propertyFrame.containsKey("@default")) {
def = JsonLdUtils.clone(propertyFrame.get("@default"));
}
if (!(def instanceof List)) {
final List<Object> tmp = new ArrayList<>();
tmp.add(def);
def = tmp;
}
final Map<String, Object> tmp1 = newMap("@preserve", def);
final List<Object> tmp2 = new ArrayList<>();
tmp2.add(tmp1);
output.put(prop, tmp2);
}
}
// add output to parent
addFrameOutput(state, parent, property, output);
}
}
}
private Boolean getFrameFlag(Map<String, Object> frame, String name, boolean thedefault) {
Object value = frame.get(name);
if (value instanceof List) {
if (!((List<Object>) value).isEmpty()) {
value = ((List<Object>) value).get(0);
}
}
if (value instanceof Map && ((Map<String, Object>) value).containsKey("@value")) {
value = ((Map<String, Object>) value).get("@value");
}
if (value instanceof Boolean) {
return (Boolean) value;
}
return thedefault;
}
/**
* Removes an existing embed.
*
* @param state
* the current framing state.
* @param id
* the @id of the embed to remove.
*/
private static void removeEmbed(FramingContext state, String id) {
// get existing embed
final Map<String, EmbedNode> embeds = state.embeds;
final EmbedNode embed = embeds.get(id);
final Object parent = embed.parent;
final String property = embed.property;
// create reference to replace embed
final Map<String, Object> node = newMap("@id", id);
// remove existing embed
if (JsonLdUtils.isNode(parent)) {
// replace subject with reference
final List<Object> newvals = new ArrayList<>();
final List<Object> oldvals = (List<Object>) ((Map<String, Object>) parent)
.get(property);
for (final Object v : oldvals) {
if (v instanceof Map && Obj.equals(((Map<String, Object>) v).get("@id"), id)) {
newvals.add(node);
} else {
newvals.add(v);
}
}
((Map<String, Object>) parent).put(property, newvals);
}
// recursively remove dependent dangling embeds
removeDependents(embeds, id);
}
private static void removeDependents(Map<String, EmbedNode> embeds, String id) {
// get embed keys as a separate array to enable deleting keys in map
for (final Map.Entry<String, EmbedNode> stringEmbedNodeEntry : embeds.entrySet()) {
final EmbedNode e = stringEmbedNodeEntry.getValue();
final Object p = e.parent != null ? e.parent : newMap();
if (!(p instanceof Map)) {
continue;
}
final String pid = (String) ((Map<String, Object>) p).get("@id");
if (Obj.equals(id, pid)) {
embeds.remove(stringEmbedNodeEntry.getKey());
removeDependents(embeds, stringEmbedNodeEntry.getKey());
}
}
}
private Map<String, Object> filterNodes(FramingContext state, Map<String, Object> nodes,
Map<String, Object> frame) throws JsonLdError {
final Map<String, Object> rval = newMap();
for (final Map.Entry<String, Object> stringObjectEntry : nodes.entrySet()) {
final Map<String, Object> element = (Map<String, Object>) stringObjectEntry.getValue();
if (element != null && filterNode(state, element, frame)) {
rval.put(stringObjectEntry.getKey(), element);
}
}
return rval;
}
private boolean filterNode(FramingContext state, Map<String, Object> node,
Map<String, Object> frame) throws JsonLdError {
final Object types = frame.get("@type");
if (types != null) {
if (!(types instanceof List)) {
throw new JsonLdError(Error.SYNTAX_ERROR, "frame @type must be an array");
}
Object nodeTypes = node.get("@type");
if (nodeTypes == null) {
nodeTypes = new ArrayList<>();
} else if (!(nodeTypes instanceof List)) {
throw new JsonLdError(Error.SYNTAX_ERROR, "node @type must be an array");
}
if (((List<Object>) types).size() == 1 && ((List<Object>) types).get(0) instanceof Map
&& ((Map<String, Object>) ((List<Object>) types).get(0)).isEmpty()) {
return !((List<Object>) nodeTypes).isEmpty();
} else {
for (final Object i : (List<Object>) nodeTypes) {
for (final Object j : (List<Object>) types) {
if (JsonLdUtils.deepCompare(i, j)) {
return true;
}
}
}
return false;
}
} else {
for (final String key : frame.keySet()) {
if ("@id".equals(key) || !isKeyword(key) && !(node.containsKey(key))) {
return false;
}
}
return true;
}
}
/**
* Adds framing output to the given parent.
*
* @param state
* the current framing state.
* @param parent
* the parent to add to.
* @param property
* the parent property.
* @param output
* the output to add.
*/
private static void addFrameOutput(FramingContext state, Object parent, String property,
Object output) {
if (parent instanceof Map) {
List<Object> prop = (List<Object>) ((Map<String, Object>) parent).get(property);
if (prop == null) {
prop = new ArrayList<>();
((Map<String, Object>) parent).put(property, prop);
}
prop.add(output);
} else {
((List) parent).add(output);
}
}
/**
* Embeds values for the given subject and property into the given output
* during the framing algorithm.
*
* @param state
* the current framing state.
* @param element
* the subject.
* @param property
* the property.
* @param output
* the output.
*/
private void embedValues(FramingContext state, Map<String, Object> element, String property,
Object output) {
// embed subject properties in output
final List<Object> objects = (List<Object>) element.get(property);
for (Object o : objects) {
// handle subject reference
if (JsonLdUtils.isNodeReference(o)) {
final String sid = (String) ((Map<String, Object>) o).get("@id");
// embed full subject if isn't already embedded
if (!state.embeds.containsKey(sid)) {
// add embed
final EmbedNode embed = new EmbedNode();
embed.parent = output;
embed.property = property;
state.embeds.put(sid, embed);
// recurse into subject
o = newMap();
Map<String, Object> s = (Map<String, Object>) this.nodeMap.get(sid);
if (s == null) {
s = newMap("@id", sid);
}
for (final Map.Entry<String, Object> stringObjectEntry : s.entrySet()) {
// copy keywords
if (isKeyword(stringObjectEntry.getKey())) {
((Map<String, Object>) o).put(stringObjectEntry.getKey(), JsonLdUtils.clone(stringObjectEntry.getValue()));
continue;
}
embedValues(state, s, stringObjectEntry.getKey(), o);
}
}
addFrameOutput(state, output, property, o);
}
// copy non-subject value
else {
addFrameOutput(state, output, property, JsonLdUtils.clone(o));
}
}
}
/***
* ____ _ __ ____ ____ _____ _ _ _ _ _ / ___|___ _ ____ _____ _ __| |_ / _|_
* __ ___ _ __ ___ | _ \| _ \| ___| / \ | | __ _ ___ _ __(_) |_| |__ _ __
* ___ | | / _ \| '_ \ \ / / _ \ '__| __| | |_| '__/ _ \| '_ ` _ \ | |_) | |
* | | |_ / _ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ | |__| (_) | | | \
* V / __/ | | |_ | _| | | (_) | | | | | | | _ <| |_| | _| / ___ \| | (_| |
* (_) | | | | |_| | | | | | | | | \____\___/|_| |_|\_/ \___|_| \__| |_| |_|
* \___/|_| |_| |_| |_| \_\____/|_| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_|
* |_| |_| |___/
*/
/**
* Helper class for node usages
*
* @author tristan
*/
private class UsagesNode {
public UsagesNode(NodeMapNode node, String property, Map<String, Object> value) {
this.node = node;
this.property = property;
this.value = value;
}
public NodeMapNode node = null;
public String property = null;
public Map<String, Object> value = null;
}
private class NodeMapNode extends LinkedHashMap<String, Object> {
public List<UsagesNode> usages = new ArrayList();
public NodeMapNode(String id) {
super();
this.put("@id", id);
}
// helper fucntion for 4.3.3
public boolean isWellFormedListNode() {
if (usages.size() != 1) {
return false;
}
int keys = 0;
if (containsKey(RDF_FIRST)) {
keys++;
if (!(get(RDF_FIRST) instanceof List && ((List<Object>) get(RDF_FIRST)).size() == 1)) {
return false;
}
}
if (containsKey(RDF_REST)) {
keys++;
if (!(get(RDF_REST) instanceof List && ((List<Object>) get(RDF_REST)).size() == 1)) {
return false;
}
}
if (containsKey("@type")) {
keys++;
if (!(get("@type") instanceof List && ((List<Object>) get("@type")).size() == 1)
&& RDF_LIST.equals(((List<Object>) get("@type")).get(0))) {
return false;
}
}
// TODO: SPEC: 4.3.3 has no mention of @id
if (containsKey("@id")) {
keys++;
}
if (keys < size()) {
return false;
}
return true;
}
// return this node without the usages variable
public Map<String, Object> serialize() {
return new LinkedHashMap<>(this);
}
}
/**
* Converts RDF statements into JSON-LD.
*
* @param dataset
* the RDF statements.
* @return A list of JSON-LD objects found in the given dataset.
* @throws JsonLdError
* If there was an error during conversion from RDF to JSON-LD.
*/
public List<Object> fromRDF(final RDFDataset dataset) throws JsonLdError {
// 1)
final Map<String, NodeMapNode> defaultGraph = new LinkedHashMap<>();
// 2)
final Map<String, Map<String, NodeMapNode>> graphMap = new LinkedHashMap<>();
graphMap.put("@default", defaultGraph);
// 3/3.1)
for (final String name : dataset.graphNames()) {
final List<RDFDataset.Quad> graph = dataset.getQuads(name);
// 3.2+3.4)
Map<String, NodeMapNode> nodeMap;
if (!graphMap.containsKey(name)) {
nodeMap = new LinkedHashMap<>();
graphMap.put(name, nodeMap);
} else {
nodeMap = graphMap.get(name);
}
// 3.3)
if (!"@default".equals(name) && !Obj.contains(defaultGraph, name)) {
defaultGraph.put(name, new NodeMapNode(name));
}
// 3.5)
for (final RDFDataset.Quad triple : graph) {
final String subject = triple.getSubject().getValue();
final String predicate = triple.getPredicate().getValue();
final RDFDataset.Node object = triple.getObject();
// 3.5.1+3.5.2)
NodeMapNode node;
if (!nodeMap.containsKey(subject)) {
node = new NodeMapNode(subject);
nodeMap.put(subject, node);
} else {
node = nodeMap.get(subject);
}
// 3.5.3)
if ((object.isIRI() || object.isBlankNode())
&& !nodeMap.containsKey(object.getValue())) {
nodeMap.put(object.getValue(), new NodeMapNode(object.getValue()));
}
// 3.5.4)
if (RDF_TYPE.equals(predicate) && (object.isIRI() || object.isBlankNode())
&& !opts.getUseRdfType()) {
JsonLdUtils.mergeValue(node, "@type", object.getValue());
continue;
}
// 3.5.5)
final Map<String, Object> value = object.toObject(opts.getUseNativeTypes());
// 3.5.6+7)
JsonLdUtils.mergeValue(node, predicate, value);
// 3.5.8)
if (object.isBlankNode() || object.isIRI()) {
// 3.5.8.1-3)
nodeMap.get(object.getValue()).usages
.add(new UsagesNode(node, predicate, value));
}
}
}
// 4)
for (final Map.Entry<String, Map<String, NodeMapNode>> stringMapEntry : graphMap.entrySet()) {
final Map<String, NodeMapNode> graph = stringMapEntry.getValue();
// 4.1)
if (!graph.containsKey(RDF_NIL)) {
continue;
}
// 4.2)
final NodeMapNode nil = graph.get(RDF_NIL);
// 4.3)
for (final UsagesNode usage : nil.usages) {
// 4.3.1)
NodeMapNode node = usage.node;
String property = usage.property;
Map<String, Object> head = usage.value;
// 4.3.2)
final List<Object> list = new ArrayList<>();
final List<String> listNodes = new ArrayList<>();
// 4.3.3)
while (RDF_REST.equals(property) && node.isWellFormedListNode()) {
// 4.3.3.1)
list.add(((List<Object>) node.get(RDF_FIRST)).get(0));
// 4.3.3.2)
listNodes.add((String) node.get("@id"));
// 4.3.3.3)
final UsagesNode nodeUsage = node.usages.get(0);
// 4.3.3.4)
node = nodeUsage.node;
property = nodeUsage.property;
head = nodeUsage.value;
// 4.3.3.5)
if (!JsonLdUtils.isBlankNode(node)) {
break;
}
}
// 4.3.4)
if (RDF_FIRST.equals(property)) {
// 4.3.4.1)
if (RDF_NIL.equals(node.get("@id"))) {
continue;
}
// 4.3.4.3)
final String headId = (String) head.get("@id");
// 4.3.4.4-5)
head = (Map<String, Object>) ((List<Object>) graph.get(headId).get(RDF_REST))
.get(0);
// 4.3.4.6)
list.remove(list.size() - 1);
listNodes.remove(listNodes.size() - 1);
}
// 4.3.5)
head.remove("@id");
// 4.3.6)
Collections.reverse(list);
// 4.3.7)
head.put("@list", list);
// 4.3.8)
for (final String nodeId : listNodes) {
graph.remove(nodeId);
}
}
}
// 5)
final List<Object> result = new ArrayList<>();
// 6)
final List<String> ids = new ArrayList<>(defaultGraph.keySet());
Collections.sort(ids);
for (final String subject : ids) {
final NodeMapNode node = defaultGraph.get(subject);
// 6.1)
if (graphMap.containsKey(subject)) {
// 6.1.1)
node.put("@graph", new ArrayList<>());
// 6.1.2)
final List<String> keys = new ArrayList<>(graphMap.get(subject).keySet());
Collections.sort(keys);
for (final String s : keys) {
final NodeMapNode n = graphMap.get(subject).get(s);
if (n.size() == 1 && n.containsKey("@id")) {
continue;
}
((List<Object>) node.get("@graph")).add(n.serialize());
}
}
// 6.2)
if (node.size() == 1 && node.containsKey("@id")) {
continue;
}
result.add(node.serialize());
}
return result;
}
/***
* ____ _ _ ____ ____ _____ _ _ _ _ _ / ___|___ _ ____ _____ _ __| |_ | |_
* ___ | _ \| _ \| ___| / \ | | __ _ ___ _ __(_) |_| |__ _ __ ___ | | / _ \|
* '_ \ \ / / _ \ '__| __| | __/ _ \ | |_) | | | | |_ / _ \ | |/ _` |/ _ \|
* '__| | __| '_ \| '_ ` _ \ | |__| (_) | | | \ V / __/ | | |_ | || (_) | |
* _ <| |_| | _| / ___ \| | (_| | (_) | | | | |_| | | | | | | | |
* \____\___/|_| |_|\_/ \___|_| \__| \__\___/ |_| \_\____/|_| /_/ \_\_|\__,
* |\___/|_| |_|\__|_| |_|_| |_| |_| |___/
*/
/**
* Adds RDF triples for each graph in the current node map to an RDF
* dataset.
*
* @return the RDF dataset.
* @throws JsonLdError
* If there was an error converting from JSON-LD to RDF.
*/
public RDFDataset toRDF() throws JsonLdError {
// TODO: make the default generateNodeMap call (i.e. without a
// graphName) create and return the nodeMap
final Map<String, Object> nodeMap = newMap();
nodeMap.put("@default", newMap());
generateNodeMap(this.value, nodeMap);
final RDFDataset dataset = new RDFDataset(this);
for (final Map.Entry<String, Object> stringObjectEntry : nodeMap.entrySet()) {
// 4.1)
if (JsonLdUtils.isRelativeIri(stringObjectEntry.getKey())) {
continue;
}
final Map<String, Object> graph = (Map<String, Object>) stringObjectEntry.getValue();
replaceBlankNode(graph);
dataset.graphToRDF(stringObjectEntry.getKey(), graph);
}
return dataset;
}
private void replaceBlankNode(Map<String, Object> graph) {
List<String> entriesToRemove = new ArrayList<>();
for (Map.Entry<String, Object> entry : graph.entrySet()) {
if (entry.getKey().startsWith("_:")) {
entriesToRemove.add(entry.getKey());
}
Object atId = graph.get("@id");
if (atId != null && atId.toString().startsWith("_:")) {
graph.put("@id", blankNodeMapping.get(atId));
}
Object obj = entry.getValue();
if (obj instanceof Map) {
replaceBlankNode((Map<String, Object>)obj);
}
if (obj instanceof List) {
for (Object o : (List)obj) {
if (o instanceof Map) {
replaceBlankNode((Map<String, Object>)o);
}
}
}
}
for (String key : entriesToRemove) {
Object tmp = graph.get(key);
graph.put(blankNodeMapping.get(key), tmp);
graph.remove(key);
}
}
/***
* _ _ _ _ _ _ _ _ _ _ _ | \ | | ___ _ __ _ __ ___ __ _| (_)______ _| |_(_)
* ___ _ __ / \ | | __ _ ___ _ __(_) |_| |__ _ __ ___ | \| |/ _ \| '__| '_ `
* _ \ / _` | | |_ / _` | __| |/ _ \| '_ \ / _ \ | |/ _` |/ _ \| '__| | __|
* '_ \| '_ ` _ \ | |\ | (_) | | | | | | | | (_| | | |/ / (_| | |_| | (_) |
* | | | / ___ \| | (_| | (_) | | | | |_| | | | | | | | | |_| \_|\___/|_|
* |_| |_| |_|\__,_|_|_/___\__,_|\__|_|\___/|_| |_| /_/ \_\_|\__, |\___/|_|
* |_|\__|_| |_|_| |_| |_| |___/
*/
/**
* Performs RDF normalization on the given JSON-LD input.
*
* @param dataset
* the expanded JSON-LD object to normalize.
* @return The normalized JSON-LD object
* @throws JsonLdError
* If there was an error while normalizing.
*/
public Object normalize(Map<String, Object> dataset) throws JsonLdError {
// create quads and map bnodes to their associated quads
final List<Object> quads = new ArrayList<>();
final Map<String, Object> bnodes = newMap();
for (String graphName : dataset.keySet()) {
final List<Map<String, Object>> triples = (List<Map<String, Object>>) dataset
.get(graphName);
if ("@default".equals(graphName)) {
graphName = null;
}
for (final Map<String, Object> quad : triples) {
if (graphName != null) {
if (graphName.indexOf("_:") == 0) {
final Map<String, Object> tmp = newMap();
tmp.put("type", "blank node");
tmp.put("value", graphName);
quad.put("name", tmp);
} else {
final Map<String, Object> tmp = newMap();
tmp.put("type", "IRI");
tmp.put("value", graphName);
quad.put("name", tmp);
}
}
quads.add(quad);
final String[] attrs = new String[] { "subject", "object", "name" };
for (final String attr : attrs) {
if (quad.containsKey(attr)
&& "blank node".equals(((Map<String, Object>) quad.get(attr))
.get("type"))) {
final String id = (String) ((Map<String, Object>) quad.get(attr))
.get("value");
if (!bnodes.containsKey(id)) {
bnodes.put(id, new LinkedHashMap<String, List<Object>>() {
{
put("quads", new ArrayList<>());
}
});
}
((List<Object>) ((Map<String, Object>) bnodes.get(id)).get("quads"))
.add(quad);
}
}
}
}
// mapping complete, start canonical naming
final NormalizeUtils normalizeUtils = new NormalizeUtils(quads, bnodes, new UniqueNamer(
"_:c14n"), opts);
return normalizeUtils.hashBlankNodes(bnodes.keySet());
}
}