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.JSONLDUtils Maven / Gradle / Ivy
package com.github.jsonldjava.core;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonParseException;
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.*;
public class JSONLDUtils {
private static final int MAX_CONTEXT_URLS = 10;
/**
* Returns whether or not the given value is a keyword (or a keyword alias).
*
* @param v the value to check.
* @param [ctx] the active context to check against.
*
* @return true if the value is a keyword, false if not.
*/
static boolean isKeyword(Object key) {
if (!isString(key)) {
return false;
}
return "@base".equals(key) ||
"@context".equals(key) ||
"@container".equals(key) ||
"@default".equals(key) ||
"@embed".equals(key) ||
"@explicit".equals(key) ||
"@graph".equals(key) ||
"@id".equals(key) ||
"@index".equals(key) ||
"@language".equals(key) ||
"@list".equals(key) ||
"@omitDefault".equals(key) ||
"@reverse".equals(key) ||
"@preserve".equals(key) ||
"@set".equals(key) ||
"@type".equals(key) ||
"@value".equals(key) ||
"@vocab".equals(key);
}
static boolean isAbsoluteIri(String value) {
return value.contains(":");
}
/**
* Adds a value to a subject. If the value is an array, all values in the
* array will be added.
*
* Note: If the value is a subject that already exists as a property of the
* given subject, this method makes no attempt to deeply merge properties.
* Instead, the value will not be added.
*
* @param subject the subject to add the value to.
* @param property the property that relates the value to the subject.
* @param value the value to add.
* @param [propertyIsArray] true if the property is always an array, false
* if not (default: false).
* @param [allowDuplicate] true if the property is a @list, false
* if not (default: false).
*/
static void addValue(Map subject, String property, Object value, boolean propertyIsArray, boolean allowDuplicate) {
if (isArray(value)) {
if (((List) value).size() == 0 && propertyIsArray && !subject.containsKey(property)) {
subject.put(property, new ArrayList());
}
for (Object val : (List) value) {
addValue(subject, property, val, propertyIsArray, allowDuplicate);
}
} else if (subject.containsKey(property)) {
// check if subject already has the value if duplicates not allowed
boolean hasValue = !allowDuplicate && hasValue(subject, property, value);
// make property an array if value not present or always an array
if (!isArray(subject.get(property)) && (!hasValue || propertyIsArray)) {
List tmp = new ArrayList();
tmp.add(subject.get(property));
subject.put(property, tmp);
}
// add new value
if (!hasValue) {
((List) subject.get(property)).add(value);
}
} else {
// add new value as a set or single value
Object tmp;
if (propertyIsArray) {
tmp = new ArrayList();
((List) tmp).add(value);
} else {
tmp = value;
}
subject.put(property, tmp);
}
}
static void addValue(Map subject, String property, Object value, boolean propertyIsArray) {
addValue(subject, property, value, propertyIsArray, true);
}
static void addValue(Map subject, String property, Object value) {
addValue(subject, property, value, false, true);
}
/**
* Creates a term definition during context processing.
*
* @param activeCtx the current active context.
* @param localCtx the local context being processed.
* @param term the term in the local context to define the mapping for.
* @param defined a map of defining/defined keys to detect cycles and prevent
* double definitions.
* @throws JSONLDProcessingError
*/
static void createTermDefinition(ActiveContext activeCtx,
Map localCtx, String term,
Map defined) throws JSONLDProcessingError {
if (defined.containsKey(term)) {
// term already defined
if (defined.get(term)) {
return;
}
// cycle detected
throw new JSONLDProcessingError("Cyclical context definition detected.")
.setType(JSONLDProcessingError.Error.CYCLICAL_CONTEXT)
.setDetail("context", localCtx)
.setDetail("term", term);
}
// now defining term
defined.put(term, false);
if (isKeyword(term)) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; keywords cannot be overridden.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("context", localCtx);
}
// remove old mapping
activeCtx.mappings.remove(term);
// get context term value
Object value = localCtx.get(term);
// clean context entry
if (value == null || (isObject(value) && // NOTE: object[key] === null will return false if the key doesn't exist in the object
((Map) value).containsKey("@id") && ((Map) value).get("@id") == null)) {
activeCtx.mappings.put(term, null);
defined.put(term, true);
return;
}
// convert short-hand value to object w/@id
if (isString(value)) {
Map tmp = new LinkedHashMap();
tmp.put("@id", value);
value = tmp;
}
if (!isObject(value)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; @context property values must be string or objects.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
Map val = (Map)value;
// create new mapping
Map mapping = new LinkedHashMap();
activeCtx.mappings.put(term, mapping);
mapping.put("reverse", false);
if (val.containsKey("@reverse")) {
if (val.containsKey("@id") || val.containsKey("@type") || val.containsKey("@language")) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; a @reverse term definition must not contain @id, @type or @language.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
if (!isString(val.get("@reverse"))) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; a @context @reverse value must be a string.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
String reverse = (String) val.get("@reverse");
// expand and add @id mapping, set @type to @id
mapping.put("@id", expandIri(activeCtx, reverse, false, true, localCtx, defined));
mapping.put("@type", "@id");
mapping.put("reverse", true);
}
else if (val.containsKey("@id")) {
if (!isString(val.get("@id"))) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; a @context @id value must be an array of strings or a string.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
String id = (String) val.get("@id");
if (id != null && !id.equals(term)) {
// expand and add @id mapping
mapping.put("@id", expandIri(activeCtx, id, false, true, localCtx, defined));
}
}
if (!mapping.containsKey("@id")) {
// see if the term has a prefix
int colon = term.indexOf(':');
if (colon != -1) {
String prefix = term.substring(0, colon);
if (localCtx.containsKey(prefix)) {
// define parent prefix
createTermDefinition(activeCtx, localCtx, prefix, defined);
}
// set @id based on prefix parent
if (activeCtx.mappings.containsKey(prefix)) {
String suffix = term.substring(colon + 1);
mapping.put("@id", (String)((Map) activeCtx.mappings.get(prefix)).get("@id") + suffix);
}
// term is an absolute IRI
else {
mapping.put("@id", term);
}
}
else {
// non-IRIs *must* define @ids if @vocab is not available
if (!activeCtx.containsKey("@vocab")) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; @context terms must define an @id.")
.setDetail("context", localCtx)
.setDetail("term", term)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
// prepend vocab to term
mapping.put("@id", (String)activeCtx.get("@vocab") + term);
}
}
// IRI mapping now defined
defined.put(term, true);
if (val.containsKey("@type")) {
if (!isString(val.get("@type"))) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; a @context @type values must be strings.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
String type = (String) val.get("@type");
if (!"@id".equals(type)) {
// expand @type to full IRI
type = expandIri(activeCtx, type, true, true, localCtx, defined);
}
mapping.put("@type", type);
}
if (val.containsKey("@container")) {
String container = (String) val.get("@container");
if (!"@list".equals(container) && !"@set".equals(container) && !"@index".equals(container) && !"@language".equals(container)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; @context @container value must be one of the following: " +
"@list, @set, @index or @language.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
if ((Boolean)mapping.get("reverse") && !"@index".equals(container)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; @context @container value for a @reverse type " +
"definition must be @index.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
// add @container to mapping
mapping.put("@container", container);
}
if (val.containsKey("@language") && !val.containsKey("@type")) {
if (val.get("@language") != null && !isString(val.get("@language"))) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; @context @language value value must be a string or null.")
.setDetail("context", localCtx)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
String language = (String) val.get("@language");
// add @language to mapping
if (language != null) {
language = language.toLowerCase();
}
mapping.put("@language", language);
}
// disallow aliasing @context and @preserve
String id = (String) mapping.get("@id");
if ("@context".equals(id) || "@preserve".equals(id)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; @context and @preserve cannot be aliased.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
}
/**
* Expands a string to a full IRI. The string may be a term, a prefix, a
* relative IRI, or an absolute IRI. The associated absolute IRI will be
* returned.
*
* @param activeCtx the current active context.
* @param value the string to expand.
* @param relativeTo options for how to resolve relative IRIs:
* base: true to resolve against the base IRI, false not to.
* vocab: true to concatenate after @vocab, false not to.
* @param localCtx the local context being processed (only given if called
* during context processing).
* @param defined a map for tracking cycles in context definitions (only given
* if called during context processing).
*
* @return the expanded value.
* @throws JSONLDProcessingError
*/
static String expandIri(ActiveContext activeCtx, String value, Boolean relativeToBase, Boolean relativeToVocab, Map localCtx, Map defined) throws JSONLDProcessingError {
// already expanded
if (value == null || isKeyword(value)) {
return value;
}
// define term dependency if not defined
if (localCtx != null && localCtx.containsKey(value) && !Boolean.TRUE.equals(defined.get(value))) {
createTermDefinition(activeCtx, localCtx, value, defined);
}
if (relativeToVocab) {
Map mapping = (Map) activeCtx.mappings.get(value);
// value is explicitly ignored with a null mapping
if (mapping == null && activeCtx.mappings.containsKey(value)) {
return null;
}
if (mapping != null) {
// value is a term
return (String) mapping.get("@id");
}
}
// split value into prefix:suffix
int colon = value.indexOf(':');
if (colon != -1) {
String prefix = value.substring(0, colon);
String suffix = value.substring(colon + 1);
// do not expand blank nodes (prefix of '_') or already-absolute IRIs (suffix of '//')
if ("_".equals(prefix) || suffix.startsWith("//")) {
return value;
}
// prefix dependency not defined, define it
if (localCtx != null && localCtx.containsKey(prefix)) {
createTermDefinition(activeCtx, localCtx, prefix, defined);
}
// use mapping if prefix is defined
if (activeCtx.mappings.containsKey(prefix)) {
String id = ((Map) activeCtx.mappings.get(prefix)).get("@id");
return id + suffix;
}
// already absolute IRI
return value;
}
// prepend vocab
if (relativeToVocab && activeCtx.containsKey("@vocab")) {
return activeCtx.get("@vocab") + value;
}
// prepend base
String rval = value;
if (relativeToBase) {
rval = prependBase(activeCtx.get("@base"), rval);
}
if (localCtx != null) {
// value must now be an absolute IRI
if (!isAbsoluteIri(rval)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; a @context value does not expand to an absolue IRI.")
.setDetail("context", localCtx)
.setDetail("value", value)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
}
return rval;
}
/**
* Prepends a base IRI to the given relative IRI.
*
* @param base the base IRI.
* @param iri the relative IRI.
*
* @return the absolute IRI.
*
* TODO: the URL class isn't as forgiving as the Node.js url parser, we may need to re-implement
* the parser here to support the flexibility required
*/
private static String prependBase(Object baseobj, String iri) {
// already an absolute IRI
if (iri.indexOf(":") != -1) {
return iri;
}
// parse base if it is a string
URL base;
if (isString(baseobj)) {
base = URL.parse((String)baseobj);
} else {
// assume base is already a URL
base = (URL)baseobj;
}
URL rel = URL.parse(iri);
// start hierarchical part
String hierPart = base.protocol;
if (!"".equals(rel.authority)) {
hierPart += "//" + rel.authority;
} else if (!"".equals(base.href)) {
hierPart += "//" + base.authority;
}
// per RFC3986 normalize
String path;
// IRI represents an absolute path
if (rel.pathname.indexOf("/") == 0) {
path = rel.pathname;
} else {
path = base.pathname;
// append relative path to the end of the last directory from base
if (!"".equals(rel.pathname)) {
path = path.substring(0, path.lastIndexOf("/") + 1);
if (path.length() > 0 && !path.endsWith("/")) {
path += "/";
}
path += rel.pathname;
}
}
// remove slashes anddots in path
path = URL.removeDotSegments(path, !"".equals(hierPart));
// add query and hash
if (!"".equals(rel.query)) {
path += "?" + rel.query;
}
if (!"".equals(rel.hash)) {
path += rel.hash;
}
String rval = hierPart + path;
if ("".equals(rval)) {
return "./";
}
return rval;
}
/**
* Expands a language map.
*
* @param languageMap the language map to expand.
*
* @return the expanded language map.
* @throws JSONLDProcessingError
*/
static List expandLanguageMap(Map languageMap) throws JSONLDProcessingError {
List rval = new ArrayList();
List keys = new ArrayList(languageMap.keySet());
Collections.sort(keys); // lexicographically sort languages
for (String key: keys) {
List val;
if (!isArray(languageMap.get(key))) {
val = new ArrayList();
val.add(languageMap.get(key));
} else {
val = (List) languageMap.get(key);
}
for (Object item : val) {
if (!isString(item)) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; language map values must be strings.")
.setDetail("languageMap", languageMap)
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR);
}
Map tmp = new LinkedHashMap();
tmp.put("@value", item);
tmp.put("@language", key.toLowerCase());
rval.add(tmp);
}
}
return rval;
}
/**
* Expands the given value by using the coercion and keyword rules in the
* given context.
*
* @param ctx the active context to use.
* @param property the property the value is associated with.
* @param value the value to expand.
* @param base the base IRI to use.
*
* @return the expanded value.
* @throws JSONLDProcessingError
*/
static Object expandValue(ActiveContext activeCtx, String activeProperty, Object value) throws JSONLDProcessingError {
// nothing to expand
if (value == null) {
return null;
}
// special-case expand @id and @type (skips '@id' expansion)
String expandedProperty = expandIri(activeCtx, activeProperty, false, true, null, null);
if ("@id".equals(expandedProperty)) {
return expandIri(activeCtx, (String)value, true, false, null, null);
} else if ("@type".equals(expandedProperty)) {
return expandIri(activeCtx, (String) value, true, true, null, null);
}
// get type definition from context
Object type = activeCtx.getContextValue(activeProperty, "@type");
// do @id expansion (automatic for @graph)
if ("@id".equals(type) || ("@graph".equals(expandedProperty) && isString(value))) {
Map tmp = new LinkedHashMap();
tmp.put("@id", expandIri(activeCtx, (String) value, true, false, null, null));
return tmp;
}
// do @id expansion w/vocab
if ("@vocab".equals(type)) {
Map tmp = new LinkedHashMap();
tmp.put("@id", expandIri(activeCtx, (String) value, true, true, null, null));
return tmp;
}
// do not expand keyword values
if (isKeyword(expandedProperty)) {
return value;
}
Map rval = new LinkedHashMap();
// other type
if (type != null) {
rval.put("@type", type);
}
// check for language tagging
else if (isString(value)) {
Object language = activeCtx.getContextValue(activeProperty, "@language");
if (language != null) {
rval.put("@language", language);
}
}
rval.put("@value", value);
return rval;
}
/**
* Throws an exception if the given value is not a valid @type value.
*
* @param v the value to check.
* @throws JSONLDProcessingError
*/
static boolean validateTypeValue(Object v) throws JSONLDProcessingError {
if (v == null) {
throw new NullPointerException("\"@type\" value cannot be null");
}
// must be a string, subject reference, or empty object
if (v instanceof String || (v instanceof Map && (((Map) v).containsKey("@id") || ((Map) v).size() == 0))) {
return true;
}
// must be an array
boolean isValid = false;
if (v instanceof List) {
isValid = true;
for (Object i : (List) v) {
if (!(i instanceof String || i instanceof Map && ((Map) i).containsKey("@id"))) {
isValid = false;
break;
}
}
}
if (!isValid) {
throw new JSONLDProcessingError(
"Invalid JSON-LD syntax; \"@type\" value must a string, a subject reference, an array of strings or subject references, or an empty object.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("value", v);
}
return true;
}
/**
* Compacts an IRI or keyword into a term or prefix if it can be. If the
* IRI has an associated value it may be passed.
*
* @param activeCtx the active context to use.
* @param iri the IRI to compact.
* @param value the value to check or null.
* @param relativeTo options for how to compact IRIs:
* vocab: true to split after @vocab, false not to.
* @param reverse true if a reverse property is being compacted, false if not.
*
* @return the compacted term, prefix, keyword alias, or the original IRI.
*/
static String compactIri(ActiveContext activeCtx, String iri, Object value, boolean relativeToVocab, boolean reverse) {
// can't compact null
if (iri == null) {
return iri;
}
// term is a keyword, default vocab to true
if (isKeyword(iri)) {
relativeToVocab = true;
}
// use inverse context to pick a term if iri is relative to vocab
if (relativeToVocab && activeCtx.getInverse().containsKey(iri)) {
String defaultLanguage = (String) activeCtx.get("@language");
if (defaultLanguage == null) {
defaultLanguage = "@none";
}
// prefer @index if available in value
List containers = new ArrayList();
if (isObject(value) && ((Map) value).containsKey("@index")) {
containers.add("@index");
}
// defaults for term selection based on type/language
String typeOrLanguage = "@language";
String typeOrLanguageValue = "@null";
if (reverse) {
typeOrLanguage = "@type";
typeOrLanguageValue = "@reverse";
containers.add("@set");
}
// choose the most specific term that works for all elements in @list
else if (isList(value)) {
// only select @list containers if @index is NOT in value
if (!((Map) value).containsKey("@index")) {
containers.add("@list");
}
List list = (List) ((Map) value).get("@list");
String commonLanguage = (list.size() == 0) ? defaultLanguage : null;
String commonType = null;
for (Object item : list) {
String itemLanguage = "@none";
String itemType = "@none";
if (isValue(item)) {
if (((Map) item).containsKey("@language")) {
itemLanguage = (String) ((Map) item).get("@language");
}
else if (((Map) item).containsKey("@type")) {
itemType = (String) ((Map) item).get("@type");
}
// plain literal
else {
itemLanguage = "@null";
}
}
else {
itemType = "@id";
}
if (commonLanguage == null) {
commonLanguage = itemLanguage;
}
else if (!itemLanguage.equals(commonLanguage) && isValue(item)) {
commonLanguage = "@none";
}
if (commonType == null) {
commonType = itemType;
}
else if (!itemType.equals(commonType)) {
commonType = "@none";
}
// there are different languages and types in the list, so choose
// the most generic term, no need to keep iterating the list
if ("@none".equals(commonLanguage) && "@none".equals(commonType)) {
break;
}
}
commonLanguage = (commonLanguage != null) ? commonLanguage : "@none";
commonType = (commonType != null) ? commonType : "@none";
if (!"@none".equals(commonType)) {
typeOrLanguage = "@type";
typeOrLanguageValue = commonType;
}
else {
typeOrLanguageValue = commonLanguage;
}
}
else {
if (isValue(value)) {
if (((Map) value).containsKey("@language") && !((Map) value).containsKey("@index")) {
containers.add("@language");
typeOrLanguageValue = (String) ((Map) value).get("@language");
}
else if (((Map) value).containsKey("@type")) {
typeOrLanguage = "@type";
typeOrLanguageValue = (String) ((Map) value).get("@type");
}
}
else {
typeOrLanguage = "@type";
typeOrLanguageValue = "@id";
}
containers.add("@set");
}
// do term selection
containers.add("@none");
String term = selectTerm(activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue);
if (term != null) {
return term;
}
}
// no term match, use @vocab if available
if (relativeToVocab) {
if (activeCtx.containsKey("@vocab")) {
// determine if vocab is a prefix of the iri
String vocab = (String) activeCtx.get("@vocab");
if (iri.indexOf(vocab) == 0 && !iri.equals(vocab)) {
// use suffix as relative iri if it is not a term in the active context
String suffix = iri.substring(vocab.length());
if (!activeCtx.mappings.containsKey(suffix)) {
return suffix;
}
}
}
}
// no term of @vocab match, check for possible CURIEs
String choice = null;
for (String term : activeCtx.mappings.keySet()) {
// skip terms with colons, they can't be prefixes
if (term.indexOf(":") != -1) {
continue;
}
// skip entries with @ids that are not partial matches
Map definition = (Map) activeCtx.mappings.get(term);
if (definition == null || iri.equals(definition.get("@id")) || iri.indexOf((String)definition.get("@id")) != 0) {
continue;
}
// a CURIE is usable if:
// 1. it has no mapping, OR
// 2. value is null, which means we're not compacting an @value, AND
// the mapping matches the IRI
String curie = term + ":" + iri.substring(((String)definition.get("@id")).length());
Boolean isUsableCurie = (!activeCtx.mappings.containsKey(curie) ||
(value == null && activeCtx.mappings.get(curie) != null &&
iri.equals(((Map) activeCtx.mappings.get(curie)).get("@id"))));
// select curie if it is shorter or the same length but lexicographically
// less than the current choice
if (isUsableCurie && (choice == null || compareShortestLeast(curie, choice) < 0)) {
choice = curie;
}
}
// return chosen curie
if (choice != null) {
return choice;
}
// compact IRI relative to base
if (!relativeToVocab) {
return removeBase(activeCtx.get("@base"), iri);
}
// return IRI as is
return iri;
}
static String compactIri(ActiveContext ctx, String iri) {
return compactIri(ctx, iri, null, false, false);
}
/**
* Removes a base IRI from the given absolute IRI.
*
* @param base the base IRI.
* @param iri the absolute IRI.
*
* @return the relative IRI if relative to base, otherwise the absolute IRI.
*/
private static String removeBase(Object baseobj, String iri) {
URL base;
if (isString(baseobj)) {
base = URL.parse((String)baseobj);
} else {
base = (URL)baseobj;
}
// establish base root
String root = "";
if (!"".equals(base.href)) {
root += (base.protocol) + "//" + base.authority;
}
// support network-path reference with empty base
else if (iri.indexOf("//") != 0) {
root += "//";
}
// IRI not relative to base
if (iri.indexOf(root) != 0) {
return iri;
}
// remove root from IRI and parse remainder
URL rel = URL.parse(iri.substring(root.length()));
// remove path segments that match
List baseSegments = _split(base.normalizedPath, "/");
List iriSegments = _split(rel.normalizedPath, "/");
while (baseSegments.size() > 0 && iriSegments.size() > 0) {
if (!baseSegments.get(0).equals(iriSegments.get(0))) {
break;
}
if (baseSegments.size() > 0) baseSegments.remove(0);
if (iriSegments.size() > 0) iriSegments.remove(0);
}
// use '../' for each non-matching base segment
String rval = "";
if (baseSegments.size() > 0) {
// don't count the last segment if it isn't a path (doesn't end in '/')
// don't count empty first segment, it means base began with '/'
if (!base.normalizedPath.endsWith("/") || "".equals(baseSegments.get(0))) {
baseSegments.remove(baseSegments.size()-1);
}
for (int i = 0; i < baseSegments.size(); ++i) {
rval += "../";
}
}
// prepend remaining segments
rval += _join(iriSegments, "/");
// add query and hash
if (!"".equals(rel.query)) {
rval += "?" + rel.query;
}
if (!"".equals(rel.hash)) {
rval += rel.hash;
}
if ("".equals(rval)) {
rval = "./";
}
return rval;
}
/**
* Removes the @preserve keywords as the last step of the framing algorithm.
*
* @param ctx the active context used to compact the input.
* @param input the framed, compacted output.
* @param options the compaction options used.
*
* @return the resulting output.
*/
static Object removePreserve(ActiveContext ctx, Object input, Options opts) {
// recurse through arrays
if (isArray(input)) {
List output = new ArrayList();
for (Object i : (List)input) {
Object result = removePreserve(ctx, i, opts);
// drop nulls from arrays
if (result != null) {
output.add(result);
}
}
input = output;
}
else if (isObject(input)) {
// remove @preserve
if (((Map) input).containsKey("@preserve")) {
if ("@null".equals(((Map) input).get("@preserve"))) {
return null;
}
return ((Map) input).get("@preserve");
}
// skip @values
if (isValue(input)) {
return input;
}
// recurse through @lists
if (isList(input)) {
((Map) input).put("@list", removePreserve(ctx, ((Map) input).get("@list"), opts));
return input;
}
// recurse through properties
for (String prop : ((Map) input).keySet()) {
Object result = removePreserve(ctx, ((Map) input).get(prop), opts);
String container = (String) ctx.getContextValue(prop, "@container");
if (opts.compactArrays && isArray(result) && ((List) result).size() == 1 && container == null) {
result = ((List) result).get(0);
}
((Map) input).put(prop, result);
}
}
return input;
}
/**
* replicate javascript .join because i'm too lazy to keep doing it manually
*
* @param iriSegments
* @param string
* @return
*/
private static String _join(List list, String joiner) {
String rval = "";
if (list.size() > 0) rval += list.get(0);
for (int i = 1 ; i < list.size() ; i++) {
rval += joiner + list.get(i);
}
return rval;
}
/**
* replicates the functionality of javascript .split, which has different results to java's String.split if there is a trailing /
*
* @param string
* @param delim
* @return
*/
private static List _split(String string, String delim) {
List rval = new ArrayList(Arrays.asList(string.split(delim)));
if (string.endsWith("/")) {
// javascript .split includes a blank entry if the string ends with the delimiter, java .split does not so we need to add it manually
rval.add("");
}
return rval;
}
/**
* equals that allows both values to be null
*
* @param object
* @param language
* @return
*/
static boolean _equals(Object a, Object b) {
if (a == null) {
return b == null;
}
return a.equals(b);
}
/**
* Compares two strings first based on length and then lexicographically.
*
* @param a the first string.
* @param b the second string.
*
* @return -1 if a < b, 1 if a > b, 0 if a == b.
*/
static int compareShortestLeast(String a, String b) {
if (a.length() < b.length()) {
return -1;
}
else if (b.length() < a.length()) {
return 1;
}
return Integer.signum(a.compareTo(b));
}
/**
* Picks the preferred compaction term from the given inverse context entry.
*
* @param activeCtx the active context.
* @param iri the IRI to pick the term for.
* @param value the value to pick the term for.
* @param containers the preferred containers.
* @param typeOrLanguage either '@type' or '@language'.
* @param typeOrLanguageValue the preferred value for '@type' or '@language'.
*
* @return the preferred term.
*/
private static String selectTerm(ActiveContext activeCtx, String iri,
Object value, List containers, String typeOrLanguage,
String typeOrLanguageValue) {
if (typeOrLanguageValue == null) {
typeOrLanguageValue = "@null";
}
// preferences for the value of @type or @language
List prefs = new ArrayList();
// determine prefs for @id based on whether or not value compacts to a term
if ((("@id").equals(typeOrLanguageValue) || "@reverse".equals(typeOrLanguageValue)) && isSubjectReference(value)) {
// prefer @reverse first
if ("@reverse".equals(typeOrLanguageValue)) {
prefs.add("@reverse");
}
// try to compact value to a term
String term = compactIri(activeCtx, (String)((Map) value).get("@id"), null, true, false);
if (activeCtx.mappings.containsKey(term) &&
activeCtx.mappings.get(term) != null &&
((Map) value).get("@id").equals(((Map) activeCtx.mappings.get(term)).get("@id"))) {
// prefer @vocab
prefs.add("@vocab");
prefs.add("@id");
} else {
// prefer @id
prefs.add("@id");
prefs.add("@vocab");
}
}
else {
prefs.add(typeOrLanguageValue);
}
prefs.add("@none");
Map containerMap = (Map) activeCtx.inverse.get(iri);
for (String container: containers) {
// if container not available in the map, continue
if (!containerMap.containsKey(container)) {
continue;
}
Map typeOrLanguageValueMap = (Map) Obj.get(containerMap, container, typeOrLanguage);
for (String pref : prefs) {
// if type/language option not available in the map, continue
if (!typeOrLanguageValueMap.containsKey(pref)) {
continue;
}
// select term
return (String) typeOrLanguageValueMap.get(pref);
}
}
return null;
}
/**
* Performs value compaction on an object with '@value' or '@id' as the only
* property.
*
* @param activeCtx the active context.
* @param activeProperty the active property that points to the value.
* @param value the value to compact.
*
* @return the compaction result.
* @throws JSONLDProcessingError
*/
static Object compactValue(ActiveContext activeCtx, String activeProperty, Object value) throws JSONLDProcessingError {
// value is a @value
if (isValue(value)) {
// get context rules
String type = (String) activeCtx.getContextValue(activeProperty, "@type");
String language = (String) activeCtx.getContextValue(activeProperty, "@language");
String container = (String) activeCtx.getContextValue(activeProperty, "@container");
// whether or not the value has an @index that must be preserved
Boolean preserveIndex = (((Map) value).containsKey("@index") && !"@index".equals(container));
// if there's no @index to preserve ...
if (!preserveIndex) {
// matching @type or @language specified in context, compact value
if ((((Map) value).containsKey("@type") && _equals((String)((Map) value).get("@type"), type)) ||
(((Map) value).containsKey("@language") && _equals((String)((Map) value).get("@language"), language))) {
// NOTE: have to check containsKey here as javascript version relies on undefined !== null
return ((Map) value).get("@value");
}
}
// return just the value of @value if all are true:
// 1. @value is the only key or @index isn't being preserved
// 2. there is no default language or @value is not a string or
// the key has a mapping with a null @language
int keyCount = ((Map) value).size();
Boolean isValueOnlyKey = (keyCount == 1 || (keyCount == 2 && ((Map) value).containsKey("@index") && !preserveIndex));
Boolean hasDefaultLanguage = activeCtx.containsKey("@language");
Boolean isValueString = isString(((Map) value).get("@value"));
Boolean hasNullMapping = activeCtx.mappings.containsKey(activeProperty) && ((Map) activeCtx.mappings.get(activeProperty)).containsKey("@language") &&
Obj.get(activeCtx.mappings, activeProperty, "@language") == null;
if (isValueOnlyKey && (!hasDefaultLanguage || !isValueString || hasNullMapping)) {
return ((Map) value).get("@value");
}
Map rval = new LinkedHashMap();
// preserve @index
if (preserveIndex) {
rval.put(compactIri(activeCtx, "@index"), ((Map) value).get("@index"));
}
// compact @type IRI
if (((Map) value).containsKey("@type")) {
rval.put(compactIri(activeCtx, "@type"), compactIri(activeCtx, (String)((Map) value).get("@type"), null, true, false));
}
// alias @language
else if (((Map) value).containsKey("@language")) {
rval.put(compactIri(activeCtx, "@language"), ((Map) value).get("@language"));
}
// alias @value
rval.put(compactIri(activeCtx, "@value"), ((Map) value).get("@value"));
return rval;
}
// value is a subject reference
String expandedProperty = expandIri(activeCtx, activeProperty, false, true, null, null);
String type = (String) activeCtx.getContextValue(activeProperty, "@type");
Object compacted = compactIri(activeCtx, (String)((Map) value).get("@id"), null, "@vocab".equals(type), false);
if ("@id".equals(type) || "@vocab".equals(type) || "@graph".equals(type)) {
return compacted;
}
Map rval = new LinkedHashMap();
rval.put(compactIri(activeCtx, "@id"), compacted);
return rval;
}
/**
* Recursively flattens the subjects in the given JSON-LD expanded input
* into a node map.
*
* @param input the JSON-LD expanded input.
* @param graphs a map of graph name to subject map.
* @param graph the name of the current graph.
* @param namer the blank node namer.
* @param name the name assigned to the current input if it is a bnode.
* @param list the list to append to, null for none.
* @throws JSONLDProcessingError
*/
static void createNodeMap(Object input, Map graphs, String graph, UniqueNamer namer, String name, List list) throws JSONLDProcessingError {
// recurce through array
if (isArray(input)) {
for (Object i : (List)input) {
createNodeMap(i, graphs, graph, namer, null, list);
}
return;
}
// add non-object to list
if (!isObject(input)) {
if (list != null) {
list.add(input);
}
return;
}
// add value to list
if (isValue(input)) {
if (((Map) input).containsKey("@type")) {
String type = (String) ((Map) input).get("@type");
// rename @type blank node
if (type.indexOf("_:") == 0) {
type = namer.getName(type);
((Map) input).put("@type", type);
}
if (!((Map) graphs.get(graph)).containsKey(type)) {
Map tmp = new LinkedHashMap();
tmp.put("@id", type);
((Map) graphs.get(graph)).put(type, tmp);
}
}
if (list != null) {
list.add(input);
}
return;
}
// NOTE: At this point, input must be a subject.
// get name for subject
if (name == null) {
name = isBlankNode(input) ? namer.getName((String)((Map) input).get("@id")) : (String)((Map) input).get("@id");
}
// add subject reference to list
if (list != null) {
Map tmp = new LinkedHashMap();
tmp.put("@id", name);
list.add(tmp);
}
// create new subject or merge into existing one
Map subjects = (Map) graphs.get(graph);
Map subject;
if (subjects.containsKey(name)) {
subject = (Map)subjects.get(name);
} else {
subject = new LinkedHashMap();
subjects.put(name, subject);
}
subject.put("@id", name);
List properties = new ArrayList(((Map) input).keySet());
Collections.sort(properties);
for (String property : properties) {
// skip @id
if ("@id".equals(property)) {
continue;
}
// handle reverse properties
if ("@reverse".equals(property)) {
Map referencedNode = new LinkedHashMap();
referencedNode.put("@id", name);
Map reverseMap = (Map) ((Map) input).get("@reverse");
for (String reverseProperty : reverseMap.keySet()) {
for (Object item : (List)reverseMap.get(reverseProperty)) {
addValue((Map) item, reverseProperty, referencedNode, true, false);
createNodeMap(item, graphs, graph, namer);
}
}
continue;
}
// recurse into graph
if ("@graph".equals(property)) {
// add graph subjects map entry
if (!graphs.containsKey(name)) {
graphs.put(name, new LinkedHashMap());
}
String g = "@merged".equals(graph) ? graph : name;
createNodeMap(((Map) input).get(property), graphs, g, namer);
continue;
}
// copy non-@type keywords
if (!"@type".equals(property) && isKeyword(property)) {
if ("@index".equals(property) && subjects.containsKey("@index")) {
throw new JSONLDProcessingError("Invalid JSON-LD syntax; conflicting @index property detected.")
.setType(JSONLDProcessingError.Error.SYNTAX_ERROR)
.setDetail("subject", subject);
}
subject.put(property, ((Map) input).get(property));
continue;
}
// iterate over objects
List objects = (List) ((Map) input).get(property);
// if property is a bnode, assign it a new id
if (property.indexOf("_:") == 0) {
property = namer.getName(property);
}
// ensure property is added for empty arrays
if (objects.size() == 0) {
addValue(subject, property, new ArrayList(), true);
continue;
}
for (Object o : objects) {
if ("@type".equals(property)) {
// rename @type blank nodes
o = (((String) o).indexOf("_:") == 0) ? namer.getName((String) o) : o;
if (!((Map) graphs.get(graph)).containsKey(o)) {
Map tmp = new LinkedHashMap();
tmp.put("@id", o);
((Map) graphs.get(graph)).put((String)o, tmp);
}
}
// handle embedded subject or subject reference
if (isSubject(o) || isSubjectReference(o)) {
// rename blank node @id
String id = isBlankNode(o) ? namer.getName((String)((Map) o).get("@id")) : (String)((Map) o).get("@id");
// add reference and recurse
Map tmp = new LinkedHashMap();
tmp.put("@id", id);
addValue(subject, property, tmp, true, false);
createNodeMap(o, graphs, graph, namer, id);
}
// handle @list
else if (isList(o)) {
List _list = new ArrayList();
createNodeMap(((Map) o).get("@list"), graphs, graph, namer, name, _list);
o = new LinkedHashMap();
((Map) o).put("@list", _list);
addValue(subject, property, o, true, false);
}
// handle @value
else {
createNodeMap(o, graphs, graph, namer, name);
addValue(subject, property, o, true, false);
}
}
}
}
static void createNodeMap(Object input, Map graphs, String graph, UniqueNamer namer, String name) throws JSONLDProcessingError {
createNodeMap(input, graphs, graph, namer, name, null);
}
static void createNodeMap(Object input, Map graphs, String graph, UniqueNamer namer) throws JSONLDProcessingError {
createNodeMap(input, graphs, graph, namer, null, null);
}
/**
* Determines if the given value is a property of the given subject.
*
* @param subject the subject to check.
* @param property the property to check.
* @param value the value to check.
*
* @return true if the value exists, false if not.
*/
static boolean hasValue(Map subject, String property, Object value) {
boolean rval = false;
if (hasProperty(subject, property)) {
Object val = subject.get(property);
boolean isList = isList(val);
if (isList || val instanceof List) {
if (isList) {
val = ((Map) val).get("@list");
}
for (Object i : (List) val) {
if (compareValues(value, i)) {
rval = true;
break;
}
}
} else if (!(value instanceof List)) {
rval = compareValues(value, val);
}
}
return rval;
}
private static boolean hasProperty(Map subject, String property) {
boolean rval = false;
if (subject.containsKey(property)) {
Object value = subject.get(property);
rval = (!(value instanceof List) || ((List) value).size() > 0);
}
return rval;
}
/**
* Compares two JSON-LD values for equality. Two JSON-LD values will be
* considered equal if:
*
* 1. They are both primitives of the same type and value.
* 2. They are both @values with the same @value, @type, and @language, OR
* 3. They both have @ids they are the same.
*
* @param v1 the first value.
* @param v2 the second value.
*
* @return true if v1 and v2 are considered equal, false if not.
*/
static boolean compareValues(Object v1, Object v2) {
if (v1.equals(v2)) {
return true;
}
if (isValue(v1) && isValue(v2) &&
_equals(((Map) v1).get("@value"), ((Map) v2).get("@value")) &&
_equals(((Map) v1).get("@type"), ((Map) v2).get("@type")) &&
_equals(((Map) v1).get("@language"), ((Map) v2).get("@language")) &&
_equals(((Map) v1).get("@index"), ((Map) v2).get("@index"))) {
return true;
}
if ((v1 instanceof Map && ((Map) v1).containsKey("@id")) && (v2 instanceof Map && ((Map) v2).containsKey("@id"))
&& ((Map) v1).get("@id").equals(((Map) v2).get("@id"))) {
return true;
}
return false;
}
/**
* Removes a value from a subject.
*
* @param subject the subject.
* @param property the property that relates the value to the subject.
* @param value the value to remove.
* @param [options] the options to use:
* [propertyIsArray] true if the property is always an array, false
* if not (default: false).
*/
static void removeValue(Map subject, String property,
Map value) {
removeValue(subject, property, value, false);
}
static void removeValue(Map subject, String property,
Map value, boolean propertyIsArray) {
// filter out value
List values = new ArrayList();
if (subject.get(property) instanceof List) {
for (Object e: ((List)subject.get(property))) {
if (!(value.equals(e))) {
values.add(value);
}
}
} else {
if (!value.equals(subject.get(property))) {
values.add(subject.get(property));
}
}
if (values.size() == 0) {
subject.remove(property);
} else if (values.size() == 1 && !propertyIsArray) {
subject.put(property, values.get(0));
} else {
subject.put(property, values);
}
}
/**
* Returns true if the given value is a blank node.
*
* @param v the value to check.
*
* @return true if the value is a blank node, false if not.
*/
static boolean isBlankNode(Object v) {
// Note: A value is a blank node if all of these hold true:
// 1. It is an Object.
// 2. If it has an @id key its value begins with '_:'.
// 3. It has no keys OR is not a @value, @set, or @list.
if (v instanceof Map) {
if (((Map)v).containsKey("@id")) {
return ((String)((Map)v).get("@id")).startsWith("_:");
} else {
return ((Map)v).size() == 0 || !(((Map)v).containsKey("@value") || ((Map)v).containsKey("@set") || ((Map)v).containsKey("@list"));
}
}
return false;
}
/**
* Returns true if the given value is a subject with properties.
*
* @param v the value to check.
*
* @return true if the value is a subject with properties, false if not.
*/
static boolean isSubject(Object v) {
// Note: A value is a subject if all of these hold true:
// 1. It is an Object.
// 2. It is not a @value, @set, or @list.
// 3. It has more than 1 key OR any existing key is not @id.
if (v instanceof Map && !(((Map) v).containsKey("@value") || ((Map) v).containsKey("@set") || ((Map) v).containsKey("@list"))) {
return ((Map) v).size() > 1 || !((Map) v).containsKey("@id");
}
return false;
}
/**
* Returns true if the given value is a subject reference.
*
* @param v the value to check.
*
* @return true if the value is a subject reference, false if not.
*/
static boolean isSubjectReference(Object v) {
// Note: A value is a subject reference if all of these hold true:
// 1. It is an Object.
// 2. It has a single key: @id.
return (v instanceof Map && ((Map) v).size() == 1 && ((Map) v).containsKey("@id"));
}
/**
* Resolves external @context URLs using the given URL resolver. Each
* instance of @context in the input that refers to a URL will be replaced
* with the JSON @context found at that URL.
*
* @param input the JSON-LD input with possible contexts.
* @param resolver(url, callback(err, jsonCtx)) the URL resolver to use.
* @param callback(err, input) called once the operation completes.
* @throws JSONLDProcessingError
*/
static void resolveContextUrls(Object input) throws JSONLDProcessingError {
resolve(input, new LinkedHashMap());
}
private static void resolve(Object input, Map cycles) throws JSONLDProcessingError {
Pattern regex = Pattern.compile("(http|https)://(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(/|/([\\w#!:.?+=&%@!\\-/]))?");
if (cycles.size() > MAX_CONTEXT_URLS) {
throw new JSONLDProcessingError("Maximum number of @context URLs exceeded.")
.setType(JSONLDProcessingError.Error.CONTEXT_URL_ERROR)
.setDetail("max", MAX_CONTEXT_URLS);
}
// for tracking the URLs to resolve
Map urls = new LinkedHashMap();
// find all URLs in the given input
if (!findContextUrls(input, urls, false)) {
// finished
findContextUrls(input, urls, true);
}
// queue all unresolved URLs
List queue = new ArrayList();
for (String url: urls.keySet()) {
if (Boolean.FALSE.equals((Boolean)urls.get(url))) {
// validate URL
if (!regex.matcher(url).matches()) {
throw new JSONLDProcessingError("Malformed URL.")
.setType(JSONLDProcessingError.Error.INVALID_URL)
.setDetail("url", url);
}
queue.add(url);
}
}
// resolve URLs in queue
int count = queue.size();
for (String url: queue) {
// check for context URL cycle
if (cycles.containsKey(url)) {
throw new JSONLDProcessingError("Cyclical @context URLs detected.")
.setType(JSONLDProcessingError.Error.CONTEXT_URL_ERROR)
.setDetail("url", url);
}
Map _cycles = (Map) clone(cycles);
_cycles.put(url, Boolean.TRUE);
try {
Map ctx = (Map)JSONUtils.fromString((String)new java.net.URL(url).getContent());
if (!ctx.containsKey("@context")) {
ctx = new LinkedHashMap();
ctx.put("@context", new LinkedHashMap());
}
resolve(ctx, _cycles);
urls.put(url, ctx.get("@context"));
count -= 1;
if (count == 0) {
findContextUrls(input, urls, true);
}
} catch (JsonParseException e) {
throw new JSONLDProcessingError("URL does not resolve to a valid JSON-LD object.")
.setType(JSONLDProcessingError.Error.INVALID_URL)
.setDetail("url", url);
} catch (MalformedURLException e) {
throw new JSONLDProcessingError("Malformed URL.")
.setType(JSONLDProcessingError.Error.INVALID_URL)
.setDetail("url", url);
} catch (IOException e) {
throw new JSONLDProcessingError("Unable to open URL.")
.setType(JSONLDProcessingError.Error.INVALID_URL)
.setDetail("url", url);
}
}
}
/**
* Finds all @context URLs in the given JSON-LD input.
*
* @param input the JSON-LD input.
* @param urls a map of URLs (url => false/@contexts).
* @param replace true to replace the URLs in the given input with the
* @contexts from the urls map, false not to.
*
* @return true if new URLs to resolve were found, false if not.
*/
private static boolean findContextUrls(Object input,
Map urls, Boolean replace) {
int count = urls.size();
if (input instanceof List) {
for (Object i: (List)input) {
findContextUrls(i, urls, replace);
}
return count < urls.size();
} else if (input instanceof Map) {
for (String key: ((Map)input).keySet()) {
if (!"@context".equals(key)) {
findContextUrls(((Map) input).get(key), urls, replace);
continue;
}
// get @context
Object ctx = ((Map) input).get(key);
// array @context
if (ctx instanceof List) {
int length = ((List) ctx).size();
for (int i = 0; i < length; i++) {
Object _ctx = ((List) ctx).get(i);
if (_ctx instanceof String) {
// replace w/@context if requested
if (replace) {
_ctx = urls.get(_ctx);
if (_ctx instanceof List) {
// add flattened context
((List)ctx).remove(i);
((List)ctx).addAll((Collection) _ctx);
i += ((List) _ctx).size();
length += ((List) _ctx).size();
} else {
((List)ctx).set(i, _ctx);
}
}
// @context URL found
else if (!urls.containsKey(_ctx)) {
urls.put((String)_ctx, Boolean.FALSE);
}
}
}
}
// string @context
else if (ctx instanceof String) {
// replace w/@context if requested
if (replace) {
((Map) input).put(key, urls.get((String)ctx));
}
// @context URL found
else if (!urls.containsKey(ctx)) {
urls.put((String)ctx, Boolean.FALSE);
}
}
}
return (count < urls.size());
}
return false;
}
static Object clone(Object value) {// throws
// CloneNotSupportedException {
Object rval = null;
if (value instanceof Cloneable) {
try {
rval = value.getClass().getMethod("clone").invoke(value);
} catch (Exception e) {
rval = e;
}
}
if (rval == null || rval instanceof Exception) {
// the object wasn't cloneable, or an error occured
if (value == null || value instanceof String || value instanceof Number || value instanceof Boolean) {
// strings numbers and booleans are immutable
rval = value;
} else {
// TODO: making this throw runtime exception so it doesn't have
// to be caught
// because simply it should never fail in the case of JSON-LD
// and means that
// the input JSON-LD is invalid
throw new RuntimeException(new CloneNotSupportedException((rval instanceof Exception ? ((Exception) rval).getMessage() : "")));
}
}
return rval;
}
/**
* Returns true if the given value is a JSON-LD Array
*
* @param v the value to check.
* @return
*/
static Boolean isArray(Object v) {
return (v instanceof List);
}
/**
* Returns true if the given value is a JSON-LD List
*
* @param v the value to check.
* @return
*/
static Boolean isList(Object v) {
return (v instanceof Map && ((Map) v).containsKey("@list"));
}
/**
* Returns true if the given value is a JSON-LD Object
*
* @param v the value to check.
* @return
*/
static Boolean isObject(Object v) {
return (v instanceof Map);
}
/**
* Returns true if the given value is a JSON-LD value
*
* @param v the value to check.
* @return
*/
static Boolean isValue(Object v) {
return (v instanceof Map && ((Map) v).containsKey("@value"));
}
/**
* Returns true if the given value is a JSON-LD string
*
* @param v the value to check.
* @return
*/
static Boolean isString(Object v) {
// TODO: should this return true for arrays of strings as well?
return (v instanceof String);
}
}