Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.github.jsonldjava.core.JSONLDProcessor Maven / Gradle / Ivy
package com.github.jsonldjava.core;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.jsonldjava.utils.JSONUtils;
import com.github.jsonldjava.utils.Obj;
import com.github.jsonldjava.utils.URL;
import static com.github.jsonldjava.core.JSONLDConsts.*;
import static com.github.jsonldjava.core.JSONLDUtils.*;
import static com.github.jsonldjava.core.RDFDatasetUtils.*;
public class JSONLDProcessor {
private static final Logger LOG = LoggerFactory.getLogger(JSONLDProcessor.class);
Options opts;
public JSONLDProcessor() {
opts = new Options("");
}
public JSONLDProcessor(Options opts) {
if (opts == null) {
opts = new Options("");
} else {
this.opts = opts;
}
}
/**
* Processes a local context and returns a new active context.
*
* @param activeCtx the current active context.
* @param localCtx the local context to process.
* @param options the context processing options.
*
* @return the new active context.
*/
ActiveContext processContext(ActiveContext activeCtx, Object localCtx) throws JSONLDProcessingError {
// TODO: get context from cache if available
// initialize the resulting context
ActiveContext rval = activeCtx.clone();
// normalize local context to an array of @context objects
if (localCtx instanceof Map && ((Map) localCtx).containsKey("@context") && ((Map) localCtx).get("@context") instanceof List) {
localCtx = ((Map) localCtx).get("@context");
}
List> ctxs;
if (localCtx instanceof List) {
ctxs = (List>) localCtx;
} else {
ctxs = new ArrayList>();
ctxs.add((Map) localCtx);
}
// process each context in order
for (Object ctx : ctxs) {
if (ctx == null) {
// reset to initial context
rval = new ActiveContext(opts);
continue;
}
// context must be an object by now, all URLs resolved before this call
if (ctx instanceof Map) {
// dereference @context key if present
if (((Map) ctx).containsKey("@context")) {
ctx = (Map) ((Map) ctx).get("@context");
}
} else {
// context must be an object by now, all URLs resolved before this call
throw new JSONLDProcessingError("@context must be an object")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("context", ctx);
}
// define context mappings for keys in local context
Map defined = new LinkedHashMap();
// helper for access to ctx as a map
Map ctxm = (Map)ctx;
// handle @base
if (ctxm.containsKey("@base")) {
Object base = ctxm.get("@base");
// reset base
if (base == null) {
base = opts.base;
}
else if (!isString(base)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; the value of \"@base\" in a " +
"@context must be a string or null.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("context", ctx);
}
else if (!"".equals(base) && !isAbsoluteIri((String)base)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; the value of \"@base\" in a " +
"@context must be an absolute IRI or the empty string.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("context", ctx);
}
base = URL.parse((String)base);
rval.put("@base", base);
defined.put("@base", true);
}
// handle @vocab
if (ctxm.containsKey("@vocab")) {
Object value = ctxm.get("@vocab");
if (value == null) {
rval.remove("@vocab");
}
else if (!isString(value)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; the value of \"@vocab\" in a " +
"@context must be a string or null.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("context", ctx);
}
else if (!isAbsoluteIri((String)value)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; the value of \"@vocab\" in a " +
"@context must be an absolute IRI.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("context", ctx);
}
else {
rval.put("@vocab", value);
}
defined.put("@vocab", true);
}
// handle @language
if (ctxm.containsKey("@language")) {
Object value = ctxm.get("@language");
if (value == null) {
rval.remove("@language");
}
else if (!isString(value)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; the value of \"@language\" in a " +
"@context must be a string or null.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("context", ctx);
}
else {
rval.put("@language", ((String)value).toLowerCase());
}
defined.put("@language", true);
}
// process all other keys
for (String key : ctxm.keySet()) {
createTermDefinition(rval, ctxm, key, defined);
}
}
// TODO: cache results
return rval;
}
/**
* Recursively expands an element using the given context. Any context in
* the element will be removed. All context URLs must have been retrieved
* before calling this method.
*
* @param activeCtx the context to use.
* @param activeProperty the property for the element, null for none.
* @param element the element to expand.
* @param options the expansion options.
* @param insideList true if the element is a list, false if not.
*
* @return the expanded value.
*
* TODO:
* - does this function always return a map, or can it also return a list, the expandedValue variable below seems to assume a map, but in javascript, `in` will just return false if the result is a list
*/
public Object expand(ActiveContext activeCtx, String activeProperty, Object element, Boolean insideList) throws JSONLDProcessingError {
// nothing to expand
if (element == null) {
return null;
}
// recursively expand array
if (element instanceof List) {
List rval = new ArrayList();
for (Object i : (List) element) {
// expand element
Object e = expand(activeCtx, activeProperty, i, insideList);
if (insideList && (isArray(e) || isList(e))) {
// lists of lists are illegal
throw new JSONLDProcessingError("Invalid JSON-LD syntax; lists of lists are not permitted.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
// drop null values
} else if (e != null) {
if (isArray(e)) {
rval.addAll((Collection extends Object>) e);
} else {
rval.add(e);
}
}
}
return rval;
}
// recursively expand object
if (isObject(element)) {
// access helper
Map elem = (Map) element;
// if element has a context, process it
if (elem.containsKey("@context")) {
activeCtx = processContext(activeCtx, elem.get("@context"));
//elem.remove("@context");
}
// expand the active property
String expandedActiveProperty = expandIri(activeCtx, activeProperty, false, true, null, null); // {vocab: true}
Object rval = new LinkedHashMap();
Map mval = (Map) rval; // to make things easier while we know rval is a map
List keys = new ArrayList(elem.keySet());
Collections.sort(keys);
for (String key : keys) {
Object value = elem.get(key);
Object expandedValue;
// skip @context
if (key.equals("@context")) {
continue;
}
// expand key to IRI
String expandedProperty = expandIri(activeCtx, key, false, true, null, null); // {vocab: true}
// drop non-absolute IRI keys that aren't keywords
if (expandedProperty == null || !(isAbsoluteIri(expandedProperty) || isKeyword(expandedProperty))) {
continue;
}
if (isKeyword(expandedProperty) && "@reverse".equals(expandedActiveProperty)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; a keyword cannot be used as a @reverse propery.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", value);
}
if ("@id".equals(expandedProperty) && !isString(value)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@id\" value must a string.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", value);
}
// validate @type value
if ("@type".equals(expandedProperty)) {
validateTypeValue(value);
}
// @graph must be an array or an object
if ("@graph".equals(expandedProperty) && !(isObject(value) || isArray(value))) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@graph\" value must be an object or an array.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", value);
}
// @value must not be an object or an array
if ("@value".equals(expandedProperty) && (value instanceof Map || value instanceof List)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@value\" value must not be an object or an array.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", value);
}
// @language must be a string
if ("@language".equals(expandedProperty) && !(value instanceof String)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@language\" value must be a string.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", value);
}
// @index must be a string
if ("@index".equals(expandedProperty) && !(value instanceof String)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@index\" value must be a string.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", value);
}
// @reverse must be an object
if ("@reverse".equals(expandedProperty)) {
if (!isObject(value)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@reverse\" value must be an object.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", value);
}
expandedValue = expand(activeCtx, "@reverse", value, insideList);
// properties double-reversed
if (expandedValue instanceof Map && ((Map) expandedValue).containsKey("@reverse")) {
// TODO: javascript seems to assume that the value of reverse will always be an object, may need to add a check here if this turns out to be the case
Map rev = (Map) ((Map) expandedValue).get("@reverse");
for (String property : rev.keySet()) {
addValue(mval, property, rev.get(property), true);
}
}
// FIXME: can this be merged with the code below to simplify?
// merge in all reversed properties
if (expandedValue instanceof Map) { // TODO: javascript doesn't make this check, can we assume expandedValue is always going to be an object?
Map reverseMap = (Map) mval.get("@reverse");
for (String property : ((Map) expandedValue).keySet()) {
if ("@reverse".equals(property)) {
continue;
}
if (reverseMap == null) {
reverseMap = new LinkedHashMap();
mval.put("@reverse", reverseMap);
}
addValue(reverseMap, property, new ArrayList(), true);
List items = (List) ((Map) expandedValue).get(property);
for (Object item : items) {
if (isValue(item) || isList(item)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@reverse\" value must not be a @value or an @list.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", expandedValue);
}
addValue(reverseMap, property, item, true);
}
}
}
continue;
}
String container = (String) activeCtx.getContextValue(key, "@container");
// handle language map container (skip if value is not an object)
if ("@language".equals(container) && isObject(value)) {
expandedValue = expandLanguageMap((Map)value);
}
// handle index container (skip if value is not an object)
else if ("@index".equals(container) && isObject(value)) {
// NOTE: implementing embeded function expandIndexMap from javascript as rolled out code here
// as it doesn't call itself and needs access to this instance's expand method.
// using eim_ prefix for variables to avoid clashes
String eim_activeProperty = key;
List eim_rval = new ArrayList();
for (String eim_key: ((Map) value).keySet()) {
List eim_val;
if (!isArray(((Map) value).get(eim_key))) {
eim_val = new ArrayList();
eim_val.add(((Map) value).get(eim_key));
} else {
eim_val = (List) ((Map) value).get(eim_key);
}
// NOTE: javascript assumes list result here, so I am as well
eim_val = (List) expand(activeCtx, eim_activeProperty, eim_val, false);
for (Object eim_item : eim_val) {
if (isObject(eim_item)) {
if (!((Map) eim_item).containsKey("@index")) {
((Map) eim_item).put("@index", eim_key);
}
eim_rval.add(eim_item);
}
}
}
expandedValue = eim_rval;
} else {
// recurse into @list or @set
Boolean isList = "@list".equals(expandedProperty);
if (isList || "@set".equals(expandedProperty)) {
String nextActiveProperty = activeProperty;
if (isList && "@graph".equals(expandedActiveProperty)) {
nextActiveProperty = null;
}
expandedValue = expand(activeCtx, nextActiveProperty, value, isList);
if (isList && isList(expandedValue)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; lists of lists are not permitted.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
}
else {
// recursively expand value with key as new active property
expandedValue = expand(activeCtx, key, value, false);
}
}
// drop null values if property is not @value
if (expandedValue == null && !"@value".equals(expandedProperty)) {
continue;
}
// convert expanded value to @list if container specified it
if (!"@list".equals(expandedProperty) && !isList(expandedValue) && "@list".equals(container)) {
// ensure expanded value is an array
Map tm = new LinkedHashMap();
List tl;
if (isArray(expandedValue)) {
tl = (List) expandedValue;
} else {
tl = new ArrayList();
tl.add(expandedValue);
}
tm.put("@list", tl);
expandedValue = tm;
}
// FIXME: can this be merged with the code above to simplify?
// merge in all reversed properties
if (Boolean.TRUE.equals(Obj.get(activeCtx.mappings, key, "reverse"))) {
Map reverseMap = new LinkedHashMap();
mval.put("@reverse", reverseMap);
if (!isArray(expandedValue)) {
List tmp = new ArrayList();
tmp.add(expandedValue);
expandedValue = tmp;
}
for (Object item : (List)expandedValue) {
if (isValue(item) || isList(item)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; \"@reverse\" value must not be a @value or an @list.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", expandedValue);
}
addValue(reverseMap, expandedProperty, item, true);
}
continue;
}
// add value for property
// use an array except for certain keywords
Boolean useArray = !("@index".equals(expandedProperty) || "@id".equals(expandedProperty) ||
"@type".equals(expandedProperty) || "@value".equals(expandedProperty) ||
"@language".equals(expandedProperty));
addValue(mval, expandedProperty, expandedValue, useArray);
}
// get property count on expanded output
int count = mval.size();
// @value must only have @language or @type
if (mval.containsKey("@value")) {
// @value must only have @language or @type
if (mval.containsKey("@type") && mval.containsKey("@language")) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; an element containing \"@value\" may not contain both \"@type\" and \"@language\".")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("element", mval);
}
int validCount = count -1;
if (mval.containsKey("@type") || mval.containsKey("@language")) {
validCount -= 1;
}
if (mval.containsKey("@index")) {
validCount -= 1;
}
if (validCount != 0) {
throw new JSONLDProcessingError(
"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\".")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("element", mval);
}
// drop null @values
if (mval.get("@value") == null) {
rval = null; mval = null;
}
// drop @language if @value isn't a string
else if (mval.containsKey("@language") && !isString(mval.get("@value"))) {
mval.remove("@language");
}
}
// convert @type to an array
else if (mval.containsKey("@type") && !isArray(mval.get("@type"))) {
List tmp = new ArrayList();
tmp.add(mval.get("@type"));
mval.put("@type", tmp);
}
// handle @set and @list
else if (mval.containsKey("@set") || mval.containsKey("@list")) {
if (count > 1 && (count != 2 && mval.containsKey("@index"))) {
throw new JSONLDProcessingError(
"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\".")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("element", mval);
}
// optimize away @set
if (mval.containsKey("@set")) {
rval = mval.get("@set");
mval = null; // result is no longer a map, so don't allow this to be used anymore
count = ((Collection)rval).size(); // TODO: i'm sure the result here should be a List, but Collection works, so it'll do for now
}
}
// drop objects with only @language
else if (mval.containsKey("@language") && count == 1) {
rval = null; mval = null;
}
// drop certain top-level object that do not occur in lists
if (isObject(rval) && !opts.keepFreeFloatingNodes && !insideList &&
(activeProperty == null || "@graph".equals(expandedActiveProperty))) {
// drop empty object or top-level @value
if (count == 0 || mval.containsKey("@value")) {
rval = null; mval = null;
} else {
// drop nodes that generate no triples
boolean hasTriples = false;
for (String key: mval.keySet()) {
if (hasTriples) {
break;
}
if (!isKeyword(key) || "@graph".equals(key) || "@type".equals(key)) {
hasTriples = true;
}
}
if (!hasTriples) {
rval = null; mval = null;
}
}
}
return rval;
}
// drop top-level scalars that are not in lists
if (!insideList && (activeProperty == null || "@graph".equals(expandIri(activeCtx, activeProperty, false, true, null, null)))) {
return null;
}
// expand element according to value expansion rules
return expandValue(activeCtx, activeProperty, element);
}
/**
* Recursively compacts an element using the given active context. All values
* must be in expanded form before this method is called.
*
* @param activeCtx the active context to use.
* @param activeProperty the compacted property associated with the element
* to compact, null for none.
* @param element the element to compact.
* @param options the compaction options.
*
* @return the compacted value.
*/
public Object compact(ActiveContext activeCtx, String activeProperty, Object element) throws JSONLDProcessingError {
// recursively compact array
if (isArray(element)) {
List rval = new ArrayList();
for (Object i : (List) element) {
// compact, dropping any null values
Object compacted = compact(activeCtx, activeProperty, i);
if (compacted != null) {
rval.add(compacted);
}
}
if (opts.compactArrays && rval.size() == 1) {
// use single element if no container is specified
Object container = activeCtx.getContextValue(activeProperty, "@container");
if (container == null) {
return rval.get(0);
}
}
return rval;
}
// recursively compact object
if (isObject(element)) {
// access helper
Map elem = (Map) element;
// do value compaction on @value and subject references
if (isValue(element) || isSubjectReference(element)) {
return compactValue(activeCtx, activeProperty, element);
}
// FIXME: avoid misuse of active property as an expanded property?
boolean insideReverse = ("@reverse".equals(activeProperty));
// process element keys in order
List keys = new ArrayList(elem.keySet());
Collections.sort(keys);
Map rval = new LinkedHashMap();
for (String expandedProperty : keys) {
Object expandedValue = elem.get(expandedProperty);
/* TODO:
// handle ignored keys
if (opts.isIgnored(key)) {
//JSONLDUtils.addValue(rval, key, value, false);
rval.put(key, value);
continue;
}
*/
// compact @id and @type(s)
if ("@id".equals(expandedProperty) || "@type".equals(expandedProperty)) {
Object compactedValue;
// compact single @id
if (isString(expandedValue)) {
compactedValue = compactIri(activeCtx, (String)expandedValue, null, "@type".equals(expandedProperty), false);
}
// expanded value must be a @type array
else {
List types = new ArrayList();
for (String i : (List) expandedValue) {
types.add(compactIri(activeCtx, i, null, true, false));
}
compactedValue = types;
}
// use keyword alias and add value
String alias = compactIri(activeCtx, expandedProperty);
addValue(rval, alias, compactedValue,
isArray(compactedValue) && ((List) expandedValue).size() == 0);
continue;
}
// handle @reverse
if ("@reverse".equals(expandedProperty)) {
// recursively compact expanded value
// TODO: i'm assuming this will always be a map due to the rest of the code
Map compactedValue = (Map) compact(activeCtx, "@reverse", expandedValue);
// handle double-reversed properties
for (String compactedProperty : compactedValue.keySet()) {
if (Boolean.TRUE.equals(Obj.get(activeCtx.mappings, compactedProperty, "reverse"))) {
if (!rval.containsKey(compactedProperty) && !opts.compactArrays) {
rval.put(compactedProperty, new ArrayList());
}
addValue(rval, compactedProperty, compactedValue.get(compactedProperty));
compactedValue.remove(compactedProperty);
}
}
if (compactedValue.size() > 0) {
// use keyword alias and add value
addValue(rval, compactIri(activeCtx, expandedProperty), compactedValue);
}
continue;
}
// handle @index property
if ("@index".equals(expandedProperty)) {
// drop @index if inside an @index container
String container = (String) activeCtx.getContextValue(activeProperty, "@container");
if ("@index".equals(container)) {
continue;
}
// use keyword alias and add value
addValue(rval, compactIri(activeCtx, expandedProperty), expandedValue);
continue;
}
// NOTE: expanded value must be an array due to expansion algorithm.
// preserve empty arrays
if (((List) expandedValue).size() == 0) {
addValue(rval, compactIri(activeCtx, expandedProperty, expandedValue, true, insideReverse), expandedValue, true);
}
// recusively process array values
for (Object expandedItem : (List) expandedValue) {
// compact property and get container type
String itemActiveProperty = compactIri(activeCtx, expandedProperty, expandedItem, true, insideReverse);
String container = (String) activeCtx.getContextValue(itemActiveProperty, "@container");
// get @list value if appropriate
boolean isList = isList(expandedItem);
Object list = null;
if (isList) {
list = ((Map) expandedItem).get("@list");
}
// recursively compact expanded item
Object compactedItem = compact(activeCtx, itemActiveProperty, isList ? list : expandedItem);
// handle @list
if (isList) {
// ensure @list value is an array
if (!isArray(compactedItem)) {
List tmp = new ArrayList();
tmp.add(compactedItem);
compactedItem = tmp;
}
if (!"@list".equals(container)) {
// wrap using @list alias
Map wrapper = new LinkedHashMap();
wrapper.put(compactIri(activeCtx, "@list"), compactedItem);
compactedItem = wrapper;
// include @index from expanded @list, if any
if (((Map) expandedItem).containsKey("@index")) {
((Map) compactedItem).put(compactIri(activeCtx, "@index"), ((Map) expandedItem).get("@index"));
}
}
// can't use @list container for more than 1 list
else if (rval.containsKey(itemActiveProperty)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD compact error; property has a \"@list\" @container " +
"rule but there is more than a single @list that matches " +
"the compacted term in the document. Compaction might mix " +
"unwanted items into the list.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
}
// handle language and index maps
if ("@language".equals(container) || "@index".equals(container)) {
// get or create the map object
Map mapObject;
if (rval.containsKey(itemActiveProperty)) {
mapObject = (Map) rval.get(itemActiveProperty);
}
else {
mapObject = new LinkedHashMap();
rval.put(itemActiveProperty, mapObject);
}
// if container is a language map, simplify compacted value to
// a simple string
if ("@language".equals(container) && isValue(compactedItem)) {
compactedItem = ((Map) compactedItem).get("@value");
}
// add compact value to map object using key from expanded value
// based on the container type
addValue(mapObject, (String)((Map) expandedItem).get(container), compactedItem);
}
else {
// use an array if: compactArrays flag is false,
// @container is @set or @list, value is an empty
// array, or key is @graph
Boolean isArray = (!opts.compactArrays || "@set".equals(container) || "@list".equals(container) ||
(isArray(compactedItem) && ((List) compactedItem).size() == 0) ||
"@list".equals(expandedProperty) || "@graph".equals(expandedProperty));
// add compact value
addValue(rval, itemActiveProperty, compactedItem, isArray);
}
}
}
return rval;
}
// only primatives remain which are already compact
return element;
}
private class FramingContext {
public Map embeds = null;
public Map graphs = null;
public Map subjects = null;
public Options options = opts;
}
/**
* Performs JSON-LD framing.
*
* @param input the expanded JSON-LD to frame.
* @param frame the expanded JSON-LD frame to use.
* @param options the framing options.
*
* @return the framed output.
* @throws JSONLDProcessingError
*/
public Object frame(Object input, Object frame) throws JSONLDProcessingError {
// create framing state
FramingContext state = new FramingContext();
//Map state = new HashMap();
//state.put("options", this.opts);
state.graphs = new LinkedHashMap();
state.graphs.put("@default", new LinkedHashMap());
state.graphs.put("@merged", new LinkedHashMap());
// produce a map of all graphs and name each bnode
// FIXME: currently uses subjects from @merged graph only
UniqueNamer namer = new UniqueNamer("_:b");
createNodeMap(input, state.graphs, "@merged", namer);
state.subjects = (Map) state.graphs.get("@merged");
// frame the subjects
List framed = new ArrayList();
List sortedKeys = new ArrayList(state.subjects.keySet());
Collections.sort(sortedKeys);
frame(state, sortedKeys, frame, 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 JSONLDProcessingError
*/
private void frame(FramingContext state, Collection subjects,
Object frame, Object parent, String property) throws JSONLDProcessingError {
// validate the frame
validateFrame(state, frame);
// NOTE: once validated we move to the function where the frame is specifically a map
frame(state, subjects, (Map)((List)frame).get(0), parent, property);
}
private void frame(FramingContext state, Collection subjects,
Map frame, Object parent, String property) throws JSONLDProcessingError {
// filter out subjects that match the frame
Map matches = filterSubjects(state, subjects, frame);
// get flags for current frame
Options options = state.options;
Boolean embedOn = (frame.containsKey("@embed")) ? (Boolean)((List)frame.get("@embed")).get(0) : options.embed;
Boolean explicicOn = (frame.containsKey("@explicit")) ? (Boolean)((List)frame.get("@explicit")).get(0) : options.explicit;
// add matches to output
List ids = new ArrayList(matches.keySet());
Collections.sort(ids);
for (String id: ids) {
// Note: In order to treat each top-level match as a compartmentalized
// result, create an independent copy of the embedded subjects map when the
// property is null, which only occurs at the top-level.
if (property == null) {
state.embeds = new LinkedHashMap();
}
// start output
Map output = new LinkedHashMap();
output.put("@id", id);
// prepare embed meta info
Map embed = new LinkedHashMap();
embed.put("parent", parent);
embed.put("property", property);
// if embed is on and there is an existing embed
if (embedOn && state.embeds.containsKey(id)) {
// only overwrite an existing embed if it has already been added to its
// parent -- otherwise its parent is somewhere up the tree from this
// embed and the embed would occur twice once the tree is added
embedOn = false;
// existing embed's parent is an array
Map existing = (Map) state.embeds.get(id);
if (isArray(existing.get("parent"))) {
for (Object o: (List)existing.get("parent")) {
if (compareValues(output, o)) {
embedOn = true;
break;
}
}
}
// existing embed's parent is an object
else if (hasValue((Map)existing.get("parent"), (String)existing.get("property"), output)) {
embedOn = true;
}
// 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, embed);
// iterate over subject properties
Map subject = (Map) matches.get(id);
List props = new ArrayList(subject.keySet());
Collections.sort(props);
for (String prop: props) {
// handle ignored keys
if (opts.isIgnored(prop)) {
output.put((String) prop, JSONLDUtils.clone(subject.get(prop)));
continue;
}
// copy keywords to output
if (isKeyword(prop)) {
output.put((String) prop, JSONLDUtils.clone(subject.get(prop)));
continue;
}
// if property isn't in the frame
if (!frame.containsKey(prop)) {
// if explicit is off, embed values
if (!explicicOn) {
embedValues(state, subject, prop, output);
}
continue;
}
// add objects
Object objects = subject.get(prop);
// TODO: i've done some crazy stuff here because i'm unsure if objects is always a list or if it can
// be a map as well. I think it's always a map, but i'll get it working like this first
for (Object i: objects instanceof List ? (List)objects : ((Map)objects).keySet()) {
Object o = objects instanceof List ? i : ((Map)objects).get(i);
// recurse into list
if (isList(o)) {
// add empty list
Map list = new LinkedHashMap();
list.put("@list", new ArrayList());
addFrameOutput(state, output, prop, list);
// add list objects
List src = (List)((Map)o).get("@list");
for (Object n: src) {
// recurse into subject reference
if (isSubjectReference(o)) {
List tmp = new ArrayList();
tmp.add(((Map)n).get("@id"));
frame(state, tmp, frame.get(prop), list, "@list");
} else {
// include other values automatcially
addFrameOutput(state, list, "@list", (Map) JSONLDUtils.clone(n));
}
}
continue;
}
// recurse into subject reference
if (isSubjectReference(o)) {
List tmp = new ArrayList();
tmp.add(((Map)o).get("@id"));
frame(state, tmp, frame.get(prop), output, prop);
} else {
// include other values automatically
addFrameOutput(state, output, prop, (Map) JSONLDUtils.clone(o));
}
}
}
// handle defaults
props = new ArrayList(frame.keySet());
Collections.sort(props);
for (String prop: props) {
// skip keywords
if (isKeyword(prop)) {
continue;
}
// if omit default is off, then include default values for properties
// that appear in the next frame but are not in the matching subject
Map next = (Map) ((List) frame.get(prop)).get(0);
boolean omitDefaultOn =
(next.containsKey("@omitDefault")) ? (Boolean)((List)next.get("@omitDefault")).get(0) : options.omitDefault ;
if (!omitDefaultOn && !output.containsKey(prop)) {
Object preserve = "@null";
if (next.containsKey("@default")) {
preserve = JSONLDUtils.clone(next.get("@default"));
}
if (!isArray(preserve)) {
List tmp = new ArrayList();
tmp.add(preserve);
preserve = tmp;
}
Map tmp1 = new LinkedHashMap();
tmp1.put("@preserve", preserve);
List tmp2 = new ArrayList();
tmp2.add(tmp1);
output.put(prop, tmp2);
}
}
// add output to parent
addFrameOutput(state, parent, property, output);
}
}
}
/**
* Embeds values for the given subject and property into the given output
* during the framing algorithm.
*
* @param state the current framing state.
* @param subject the subject.
* @param property the property.
* @param output the output.
*/
private void embedValues(FramingContext state,
Map subject, String property, Object output) {
// embed subject properties in output
Object objects = subject.get(property);
// TODO: more craziness due to lack of knowledge about whether objects should
// be an array or an object
for (Object i: objects instanceof List ? (List)objects : ((Map)objects).keySet()) {
Object o = objects instanceof List ? i : ((Map)objects).get(i);
// recurse into @list
if (isList(o)) {
Map list = new LinkedHashMap();
list.put("@list", new ArrayList());
addFrameOutput(state, output, property, list);
embedValues(state, (Map)o, "@list", list.get("@list"));
return;
}
// handle subject reference
if (isSubjectReference(o)) {
String id = (String) ((Map) o).get("@id");
// embed full subject if isn't already embedded
if (!state.embeds.containsKey(id)) {
// add embed
Map embed = new LinkedHashMap();
embed.put("parent", output);
embed.put("property", property);
state.embeds.put(id, embed);
// recurse into subject
o = new LinkedHashMap();
Map s = (Map) state.subjects.get(id);
for (String prop: s.keySet()) {
// copy keywords
if (isKeyword(prop) || opts.isIgnored(prop)) {
((Map) o).put(prop, JSONLDUtils.clone(s.get(prop)));
continue;
}
embedValues(state, s, prop, o);
}
}
addFrameOutput(state, output, property, o);
}
// copy non-subject value
else {
addFrameOutput(state, output, property, JSONLDUtils.clone(o));
}
}
}
/**
* 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 (isObject(parent)) {
addValue((Map)parent, property, output, true);
} else {
((List)parent).add(output);
}
}
/**
* 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
Map embeds = state.embeds;
Map embed = (Map) embeds.get(id);
Object parent = embed.get("parent");
String property = (String) embed.get("property");
// create reference to replace embed
Map subject = new LinkedHashMap();
subject.put("@id", id);
// remove existing embed
if (isArray(parent)) {
// replace subject with reference
for (int i = 0; i < ((List)parent).size(); i++) {
if (compareValues(((List)parent).get(i), subject)) {
((List)parent).set(i, subject);
break;
}
}
} else {
// replace subject with reference
removeValue(((Map)parent), property, subject, ((Map) parent).get(property) instanceof List);
addValue(((Map)parent), property, subject, ((Map) parent).get(property) instanceof List);
}
// recursively remove dependent dangling embeds
removeDependents(embeds, id);
}
private static void removeDependents(Map embeds, String id) {
// get embed keys as a separate array to enable deleting keys in map
Set ids = embeds.keySet();
for (String next: ids) {
if (embeds.containsKey(next) &&
((Map) embeds.get(next)).get("parent") instanceof Map &&
id.equals(((Map) ((Map) embeds.get(next)).get("parent")).get("@id"))) {
embeds.remove(next);
removeDependents(embeds, next);
}
}
}
/**
* Returns a map of all of the subjects that match a parsed frame.
*
* @param state the current framing state.
* @param subjects the set of subjects to filter.
* @param frame the parsed frame.
*
* @return all of the matched subjects.
*/
private static Map filterSubjects(FramingContext state,
Collection subjects, Map frame) {
// filter subjects in @id order
Map rval = new LinkedHashMap();
for (String id: subjects) {
Map subject = (Map) state.subjects.get(id);
if (filterSubject(subject, frame)) {
rval.put(id, subject);
}
}
return rval;
}
/**
* Returns true if the given subject matches the given frame.
*
* @param subject the subject to check.
* @param frame the frame to check.
*
* @return true if the subject matches, false if not.
*/
private static boolean filterSubject(Map subject, Map frame) {
// check @type (object value means 'any' type, fall through to ducktyping)
Object t = frame.get("@type");
// TODO: it seems @type should always be a list
if (frame.containsKey("@type") && !(t instanceof List && ((List)t).size() == 1 && ((List)t).get(0) instanceof Map)) {
for (Object i: (List)t) {
if (hasValue(subject, "@type", i)) {
return true;
}
}
return false;
}
// check ducktype
for (String key: frame.keySet()) {
if ("@id".equals(key) || !isKeyword(key) && !(subject.containsKey(key))) {
return false;
}
}
return true;
}
/**
* Validates a JSON-LD frame, throwing an exception if the frame is invalid.
*
* @param state the current frame state.
* @param frame the frame to validate.
* @throws JSONLDProcessingError
*/
private static void validateFrame(FramingContext state, Object frame) throws JSONLDProcessingError {
if (!(frame instanceof List) || ((List)frame).size() != 1 || !(((List) frame).get(0) instanceof Map)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; a JSON-LD frame must be a single object.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("frame", frame);
}
}
/**
* Performs RDF normalization on the given JSON-LD input.
*
* @param input the expanded JSON-LD object to normalize.
* @param options the normalization options.
* @param callback(err, normalized) called once the operation completes.
* @throws JSONLDProcessingError
*/
public Object normalize(Map dataset) throws JSONLDProcessingError {
// create quads and map bnodes to their associated quads
List quads = new ArrayList();
Map bnodes = new LinkedHashMap();
for (String graphName : dataset.keySet()) {
List> triples = (List>) dataset.get(graphName);
if ("@default".equals(graphName)) {
graphName = null;
}
for (Map quad : triples) {
if (graphName != null) {
if (graphName.indexOf("_:") == 0) {
Map tmp = new LinkedHashMap();
tmp.put("type", "blank node");
tmp.put("value", graphName);
quad.put("name", tmp);
} else {
Map tmp = new LinkedHashMap();
tmp.put("type", "IRI");
tmp.put("value", graphName);
quad.put("name", tmp);
}
}
quads.add(quad);
String[] attrs = new String[] { "subject", "object", "name" };
for (String attr : attrs) {
if (quad.containsKey(attr) && "blank node".equals(((Map) quad.get(attr)).get("type"))) {
String id = (String)((Map) quad.get(attr)).get("value");
if (!bnodes.containsKey(id)) {
bnodes.put(id, new LinkedHashMap>() {{
put("quads", new ArrayList());
}});
}
((List) ((Map) bnodes.get(id)).get("quads")).add(quad);
}
}
}
}
// mapping complete, start canonical naming
NormalizeUtils normalizeUtils = new NormalizeUtils(quads, bnodes, new UniqueNamer("_:c14n"), opts);
return normalizeUtils.hashBlankNodes(bnodes.keySet());
}
/**
* Adds RDF triples for each graph in the given node map to an RDF dataset.
*
* @param nodeMap the node map.
*
* @return the RDF dataset.
*/
public Map toRDF(Map nodeMap) {
UniqueNamer namer = new UniqueNamer("_:b");
Map dataset = new LinkedHashMap();
for (String graphName : nodeMap.keySet()) {
Map graph = (Map) nodeMap.get(graphName);
if (graphName.indexOf("_:") == 0) {
graphName = namer.getName(graphName);
}
dataset.put(graphName, graphToRDF(graph, namer));
}
return dataset;
}
/**
* Converts RDF statements into JSON-LD.
*
* @param statements the RDF statements.
* @param options the RDF conversion options.
* @param callback(err, output) called once the operation completes.
* @throws JSONLDProcessingError
*/
public List fromRDF(final Map dataset) throws JSONLDProcessingError {
final Map defaultGraph = new LinkedHashMap();
final Map> graphMap = new LinkedHashMap>() {{
put("@default", defaultGraph);
}};
// For each graph in RDF dataset
for (final String name : dataset.keySet()) {
final List> graph = (List>)dataset.get(name);
// If graph map has no name member, create one and set its value to an empty JSON object.
Map nodeMap;
if (!graphMap.containsKey(name)) {
nodeMap = new LinkedHashMap();
graphMap.put(name, nodeMap);
} else {
nodeMap = graphMap.get(name);
}
// If graph is not the default graph and default graph does not have a name member, create such
// a member and initialize its value to a new JSON object with a single member @id whose value is name.
if (!"@default".equals(name) && !Obj.contains(defaultGraph, name)) {
Obj.put(defaultGraph, name, new LinkedHashMap() {{
put("@id", name);
}});
}
// For each RDF triple in graph consisting of subject, predicate, and object
for (Map triple: graph) {
final String subject = (String)Obj.get(triple, "subject", "value");
final String predicate = (String)Obj.get(triple, "predicate", "value");
final Map object = (Map) triple.get("object");
// If node map does not have a subject member, create one and initialize its value to a new JSON object
// consisting of a single member @id whose value is set to subject.
Map node;
if (!nodeMap.containsKey(subject)) {
node = new LinkedHashMap() {{
put("@id", subject);
}};
nodeMap.put(subject, node);
} else {
node = (Map) nodeMap.get(subject);
}
// If object is an IRI or blank node identifier, does not equal rdf:nil, and node map does not have an object member,
// create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
if (("IRI".equals(object.get("type")) || ((String)object.get("value")).startsWith("_:"))
&& !RDF_NIL.equals(object.get("value")) && !nodeMap.containsKey(object.get("value"))) {
nodeMap.put((String)object.get("value"), new LinkedHashMap() {{
put("@id", object.get("value"));
}});
}
// If predicate equals rdf:type, and object is an IRI or blank node identifier, append object to the value of the
// @type member of node. If no such member exists, create one and initialize it to an array whose only item is object.
// Finally, continue to the next RDF triple
if (RDF_TYPE.equals(predicate) && ("IRI".equals(object.get("type")) || ((String)object.get("value")).startsWith("_:"))) {
addValue(node, "@type", object.get("value"), true);
continue;
}
// If object equals rdf:nil and predicate does not equal rdf:rest, set value to a new JSON object
// consisting of a single member @list whose value is set to an empty array.
Map value;
if (RDF_NIL.equals(object.get("value")) && !RDF_REST.equals(predicate)) {
value = new LinkedHashMap() {{
put("@list", new ArrayList());
}};
} else {
// Otherwise, set value to the result of using the RDF to Object Conversion algorithm, passing object and use native types.
value = RDFToObject(object, opts.useNativeTypes);
}
// If node does not have an predicate member, create one and initialize its value to an empty array.
// Add a reference to value to the to the array associated with the predicate member of node.
addValue(node, predicate, value, true);
// If object is a blank node identifier and predicate equals neither rdf:first nor rdf:rest, it might represent the head of a RDF list
if ("blank node".equals(object.get("type")) && !RDF_FIRST.equals(predicate) && !RDF_REST.equals(predicate)) {
// If the object member of node map has an usages member, add a reference to value to it;
// otherwise create such a member and set its value to an array whose only item is a reference to value.
addValue((Map)nodeMap.get(object.get("value")), "usages", value, true);
}
}
}
// build @lists
for (String name: graphMap.keySet()) {
Map graph = graphMap.get(name);
List subjects = new ArrayList(graph.keySet());
for (String subj: subjects) {
// If graph object does not have a subj member, it has been removed as it was part of a list. Continue with the next subj.
if (!graph.containsKey(subj)) {
continue;
}
// If node has no usages member or its value is not an array consisting of one item, continue with the next subj.
Map node = (Map) graph.get(subj);
if (!node.containsKey("usages") || !(node.get("usages") instanceof List) || ((List)node.get("usages")).size() != 1) {
continue;
}
Map value = (Map) ((List)node.get("usages")).get(0);
List list = new ArrayList();
List listNodes = new ArrayList();
String subject = subj;
while (!RDF_NIL.equals(subject) && list != null) {
// If node is null; the value of its @id member does not begin with _: ...
boolean test = node == null || ((String)node.get("@id")).indexOf("_:") != 0;
if (!test) {
int cnt = 0;
for (String i : new String[] { "@id", "usages", RDF_FIRST, RDF_REST }) {
if (node.containsKey(i)) {
cnt++;
}
}
// it has members other than @id, usages, rdf:first, and rdf:rest ...
test = (node.keySet().size() > cnt);
if (!test) {
// the value of its rdf:first member is not an array consisting of a single item ...
test = !(node.get(RDF_FIRST) instanceof List) || ((List)node.get(RDF_FIRST)).size() != 1;
if (!test) {
// or the value of its rdf:rest member is not an array containing a single item which is a JSON object that has an @id member ...
test = !(node.get(RDF_REST) instanceof List) || ((List)node.get(RDF_REST)).size() != 1;
if (!test) {
Object o = ((List)node.get(RDF_REST)).get(0);
test = (!(o instanceof Map && ((Map) o).containsKey("@id")));
}
}
}
}
if (test) {
// it is not a valid list node. Set list to null
list = null;
} else {
list.add(((List) node.get(RDF_FIRST)).get(0));
listNodes.add((String) node.get("@id"));
subject = (String) ((Map) ((List) node.get(RDF_REST)).get(0)).get("@id");
node = (Map) graph.get(subject);
if (listNodes.contains(subject)) {
list = null;
}
}
}
// If list is null, continue with the next subj.
if (list == null) {
continue;
}
// Remove the @id member from value.
value.remove("@id");
// Add an @list member to value and initialize it to list.
value.put("@list", list);
for (String subject_ : listNodes) {
graph.remove(subject_);
}
}
}
List result = new ArrayList();
List ids = new ArrayList(defaultGraph.keySet());
Collections.sort(ids);
for (String subject : ids) {
Map node = (Map) defaultGraph.get(subject);
if (graphMap.containsKey(subject)) {
node.put("@graph", new ArrayList());
List keys = new ArrayList(graphMap.get(subject).keySet());
Collections.sort(keys);
for (String s : keys) {
Map