boomerang-server
Version:
LDApp based BoomerangJS server
1,652 lines (1,528 loc) • 214 kB
JavaScript
/**
* A JavaScript implementation of the JSON-LD API.
*
* @author Dave Longley
*
* BSD 3-Clause License
* Copyright (c) 2011-2014 Digital Bazaar, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of the Digital Bazaar, Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function() {
// determine if in-browser or using node.js
var _nodejs = (
typeof process !== 'undefined' && process.versions && process.versions.node);
var _browser = !_nodejs &&
(typeof window !== 'undefined' || typeof self !== 'undefined');
if(_browser) {
if(typeof global === 'undefined') {
if(typeof window !== 'undefined') {
global = window;
} else if(typeof self !== 'undefined') {
global = self;
} else if(typeof $ !== 'undefined') {
global = $;
}
}
}
// attaches jsonld API to the given object
var wrapper = function(jsonld) {
/* Core API */
/**
* Performs JSON-LD compaction.
*
* @param input the JSON-LD input to compact.
* @param ctx the context to compact with.
* @param [options] options to use:
* [base] the base IRI to use.
* [compactArrays] true to compact arrays to single values when
* appropriate, false not to (default: true).
* [graph] true to always output a top-level graph (default: false).
* [expandContext] a context to expand with.
* [skipExpansion] true to assume the input is expanded and skip
* expansion, false not to, defaults to false.
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, compacted, ctx) called once the operation completes.
*/
jsonld.compact = function(input, ctx, options, callback) {
if(arguments.length < 2) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not compact, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
if(ctx === null) {
return jsonld.nextTick(function() {
callback(new JsonLdError(
'The compaction context must not be null.',
'jsonld.CompactError', {code: 'invalid local context'}));
});
}
// nothing to compact
if(input === null) {
return jsonld.nextTick(function() {
callback(null, null);
});
}
// set default options
if(!('base' in options)) {
options.base = (typeof input === 'string') ? input : '';
}
if(!('compactArrays' in options)) {
options.compactArrays = true;
}
if(!('graph' in options)) {
options.graph = false;
}
if(!('skipExpansion' in options)) {
options.skipExpansion = false;
}
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
if(!('link' in options)) {
options.link = false;
}
if(options.link) {
// force skip expansion when linking, "link" is not part of the public
// API, it should only be called from framing
options.skipExpansion = true;
}
var expand = function(input, options, callback) {
jsonld.nextTick(function() {
if(options.skipExpansion) {
return callback(null, input);
}
jsonld.expand(input, options, callback);
});
};
// expand input then do compaction
expand(input, options, function(err, expanded) {
if(err) {
return callback(new JsonLdError(
'Could not expand input before compaction.',
'jsonld.CompactError', {cause: err}));
}
// process context
var activeCtx = _getInitialContext(options);
jsonld.processContext(activeCtx, ctx, options, function(err, activeCtx) {
if(err) {
return callback(new JsonLdError(
'Could not process context before compaction.',
'jsonld.CompactError', {cause: err}));
}
var compacted;
try {
// do compaction
compacted = new Processor().compact(activeCtx, null, expanded, options);
} catch(ex) {
return callback(ex);
}
cleanup(null, compacted, activeCtx, options);
});
});
// performs clean up after compaction
function cleanup(err, compacted, activeCtx, options) {
if(err) {
return callback(err);
}
if(options.compactArrays && !options.graph && _isArray(compacted)) {
if(compacted.length === 1) {
// simplify to a single item
compacted = compacted[0];
} else if(compacted.length === 0) {
// simplify to an empty object
compacted = {};
}
} else if(options.graph && _isObject(compacted)) {
// always use array if graph option is on
compacted = [compacted];
}
// follow @context key
if(_isObject(ctx) && '@context' in ctx) {
ctx = ctx['@context'];
}
// build output context
ctx = _clone(ctx);
if(!_isArray(ctx)) {
ctx = [ctx];
}
// remove empty contexts
var tmp = ctx;
ctx = [];
for(var i = 0; i < tmp.length; ++i) {
if(!_isObject(tmp[i]) || Object.keys(tmp[i]).length > 0) {
ctx.push(tmp[i]);
}
}
// remove array if only one context
var hasContext = (ctx.length > 0);
if(ctx.length === 1) {
ctx = ctx[0];
}
// add context and/or @graph
if(_isArray(compacted)) {
// use '@graph' keyword
var kwgraph = _compactIri(activeCtx, '@graph');
var graph = compacted;
compacted = {};
if(hasContext) {
compacted['@context'] = ctx;
}
compacted[kwgraph] = graph;
} else if(_isObject(compacted) && hasContext) {
// reorder keys so @context is first
var graph = compacted;
compacted = {'@context': ctx};
for(var key in graph) {
compacted[key] = graph[key];
}
}
callback(null, compacted, activeCtx);
}
};
/**
* Performs JSON-LD expansion.
*
* @param input the JSON-LD input to expand.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [keepFreeFloatingNodes] true to keep free-floating nodes,
* false not to, defaults to false.
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, expanded) called once the operation completes.
*/
jsonld.expand = function(input, options, callback) {
if(arguments.length < 1) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not expand, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// set default options
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
if(!('keepFreeFloatingNodes' in options)) {
options.keepFreeFloatingNodes = false;
}
jsonld.nextTick(function() {
// if input is a string, attempt to dereference remote document
if(typeof input === 'string') {
var done = function(err, remoteDoc) {
if(err) {
return callback(err);
}
try {
if(!remoteDoc.document) {
throw new JsonLdError(
'No remote document found at the given URL.',
'jsonld.NullRemoteDocument');
}
if(typeof remoteDoc.document === 'string') {
remoteDoc.document = JSON.parse(remoteDoc.document);
}
} catch(ex) {
return callback(new JsonLdError(
'Could not retrieve a JSON-LD document from the URL. URL ' +
'dereferencing not implemented.', 'jsonld.LoadDocumentError', {
code: 'loading document failed',
cause: ex,
remoteDoc: remoteDoc
}));
}
expand(remoteDoc);
};
var promise = options.documentLoader(input, done);
if(promise && 'then' in promise) {
promise.then(done.bind(null, null), done);
}
return;
}
// nothing to load
expand({contextUrl: null, documentUrl: null, document: input});
});
function expand(remoteDoc) {
// set default base
if(!('base' in options)) {
options.base = remoteDoc.documentUrl || '';
}
// build meta-object and retrieve all @context URLs
var input = {
document: _clone(remoteDoc.document),
remoteContext: {'@context': remoteDoc.contextUrl}
};
if('expandContext' in options) {
var expandContext = _clone(options.expandContext);
if(typeof expandContext === 'object' && '@context' in expandContext) {
input.expandContext = expandContext;
} else {
input.expandContext = {'@context': expandContext};
}
}
_retrieveContextUrls(input, options, function(err, input) {
if(err) {
return callback(err);
}
var expanded;
try {
var processor = new Processor();
var activeCtx = _getInitialContext(options);
var document = input.document;
var remoteContext = input.remoteContext['@context'];
// process optional expandContext
if(input.expandContext) {
activeCtx = processor.processContext(
activeCtx, input.expandContext['@context'], options);
}
// process remote context from HTTP Link Header
if(remoteContext) {
activeCtx = processor.processContext(
activeCtx, remoteContext, options);
}
// expand document
expanded = processor.expand(
activeCtx, null, document, options, false);
// optimize away @graph with no other properties
if(_isObject(expanded) && ('@graph' in expanded) &&
Object.keys(expanded).length === 1) {
expanded = expanded['@graph'];
} else if(expanded === null) {
expanded = [];
}
// normalize to an array
if(!_isArray(expanded)) {
expanded = [expanded];
}
} catch(ex) {
return callback(ex);
}
callback(null, expanded);
});
}
};
/**
* Performs JSON-LD flattening.
*
* @param input the JSON-LD to flatten.
* @param ctx the context to use to compact the flattened output, or null.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, flattened) called once the operation completes.
*/
jsonld.flatten = function(input, ctx, options, callback) {
if(arguments.length < 1) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not flatten, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
} else if(typeof ctx === 'function') {
callback = ctx;
ctx = null;
options = {};
}
options = options || {};
// set default options
if(!('base' in options)) {
options.base = (typeof input === 'string') ? input : '';
}
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
// expand input
jsonld.expand(input, options, function(err, _input) {
if(err) {
return callback(new JsonLdError(
'Could not expand input before flattening.',
'jsonld.FlattenError', {cause: err}));
}
var flattened;
try {
// do flattening
flattened = new Processor().flatten(_input);
} catch(ex) {
return callback(ex);
}
if(ctx === null) {
return callback(null, flattened);
}
// compact result (force @graph option to true, skip expansion)
options.graph = true;
options.skipExpansion = true;
jsonld.compact(flattened, ctx, options, function(err, compacted) {
if(err) {
return callback(new JsonLdError(
'Could not compact flattened output.',
'jsonld.FlattenError', {cause: err}));
}
callback(null, compacted);
});
});
};
/**
* Performs JSON-LD framing.
*
* @param input the JSON-LD input to frame.
* @param frame the JSON-LD frame to use.
* @param [options] the framing options.
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [embed] default @embed flag: '@last', '@always', '@never', '@link'
* (default: '@last').
* [explicit] default @explicit flag (default: false).
* [requireAll] default @requireAll flag (default: true).
* [omitDefault] default @omitDefault flag (default: false).
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, framed) called once the operation completes.
*/
jsonld.frame = function(input, frame, options, callback) {
if(arguments.length < 2) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not frame, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// set default options
if(!('base' in options)) {
options.base = (typeof input === 'string') ? input : '';
}
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
if(!('embed' in options)) {
options.embed = '@last';
}
options.explicit = options.explicit || false;
if(!('requireAll' in options)) {
options.requireAll = true;
}
options.omitDefault = options.omitDefault || false;
jsonld.nextTick(function() {
// if frame is a string, attempt to dereference remote document
if(typeof frame === 'string') {
var done = function(err, remoteDoc) {
if(err) {
return callback(err);
}
try {
if(!remoteDoc.document) {
throw new JsonLdError(
'No remote document found at the given URL.',
'jsonld.NullRemoteDocument');
}
if(typeof remoteDoc.document === 'string') {
remoteDoc.document = JSON.parse(remoteDoc.document);
}
} catch(ex) {
return callback(new JsonLdError(
'Could not retrieve a JSON-LD document from the URL. URL ' +
'dereferencing not implemented.', 'jsonld.LoadDocumentError', {
code: 'loading document failed',
cause: ex,
remoteDoc: remoteDoc
}));
}
doFrame(remoteDoc);
};
var promise = options.documentLoader(frame, done);
if(promise && 'then' in promise) {
promise.then(done.bind(null, null), done);
}
return;
}
// nothing to load
doFrame({contextUrl: null, documentUrl: null, document: frame});
});
function doFrame(remoteFrame) {
// preserve frame context and add any Link header context
var frame = remoteFrame.document;
var ctx;
if(frame) {
ctx = frame['@context'];
if(remoteFrame.contextUrl) {
if(!ctx) {
ctx = remoteFrame.contextUrl;
} else if(_isArray(ctx)) {
ctx.push(remoteFrame.contextUrl);
} else {
ctx = [ctx, remoteFrame.contextUrl];
}
frame['@context'] = ctx;
} else {
ctx = ctx || {};
}
} else {
ctx = {};
}
// expand input
jsonld.expand(input, options, function(err, expanded) {
if(err) {
return callback(new JsonLdError(
'Could not expand input before framing.',
'jsonld.FrameError', {cause: err}));
}
// expand frame
var opts = _clone(options);
opts.isFrame = true;
opts.keepFreeFloatingNodes = true;
jsonld.expand(frame, opts, function(err, expandedFrame) {
if(err) {
return callback(new JsonLdError(
'Could not expand frame before framing.',
'jsonld.FrameError', {cause: err}));
}
var framed;
try {
// do framing
framed = new Processor().frame(expanded, expandedFrame, opts);
} catch(ex) {
return callback(ex);
}
// compact result (force @graph option to true, skip expansion,
// check for linked embeds)
opts.graph = true;
opts.skipExpansion = true;
opts.link = {};
jsonld.compact(framed, ctx, opts, function(err, compacted, ctx) {
if(err) {
return callback(new JsonLdError(
'Could not compact framed output.',
'jsonld.FrameError', {cause: err}));
}
// get graph alias
var graph = _compactIri(ctx, '@graph');
// remove @preserve from results
opts.link = {};
compacted[graph] = _removePreserve(ctx, compacted[graph], opts);
callback(null, compacted);
});
});
});
}
};
/**
* **Experimental**
*
* Links a JSON-LD document's nodes in memory.
*
* @param input the JSON-LD document to link.
* @param ctx the JSON-LD context to apply.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, linked) called once the operation completes.
*/
jsonld.link = function(input, ctx, options, callback) {
// API matches running frame with a wildcard frame and embed: '@link'
// get arguments
var frame = {};
if(ctx) {
frame['@context'] = ctx;
}
frame['@embed'] = '@link';
jsonld.frame(input, frame, options, callback);
};
/**
* **Deprecated**
*
* Performs JSON-LD objectification.
*
* @param input the JSON-LD document to objectify.
* @param ctx the JSON-LD context to apply.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, linked) called once the operation completes.
*/
jsonld.objectify = function(input, ctx, options, callback) {
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// set default options
if(!('base' in options)) {
options.base = (typeof input === 'string') ? input : '';
}
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
// expand input
jsonld.expand(input, options, function(err, _input) {
if(err) {
return callback(new JsonLdError(
'Could not expand input before linking.',
'jsonld.LinkError', {cause: err}));
}
var flattened;
try {
// flatten the graph
flattened = new Processor().flatten(_input);
} catch(ex) {
return callback(ex);
}
// compact result (force @graph option to true, skip expansion)
options.graph = true;
options.skipExpansion = true;
jsonld.compact(flattened, ctx, options, function(err, compacted, ctx) {
if(err) {
return callback(new JsonLdError(
'Could not compact flattened output before linking.',
'jsonld.LinkError', {cause: err}));
}
// get graph alias
var graph = _compactIri(ctx, '@graph');
var top = compacted[graph][0];
var recurse = function(subject) {
// can't replace just a string
if(!_isObject(subject) && !_isArray(subject)) {
return;
}
// bottom out recursion on re-visit
if(_isObject(subject)) {
if(recurse.visited[subject['@id']]) {
return;
}
recurse.visited[subject['@id']] = true;
}
// each array element *or* object key
for(var k in subject) {
var obj = subject[k];
var isid = (jsonld.getContextValue(ctx, k, '@type') === '@id');
// can't replace a non-object or non-array unless it's an @id
if(!_isArray(obj) && !_isObject(obj) && !isid) {
continue;
}
if(_isString(obj) && isid) {
subject[k] = obj = top[obj];
recurse(obj);
} else if(_isArray(obj)) {
for(var i = 0; i < obj.length; ++i) {
if(_isString(obj[i]) && isid) {
obj[i] = top[obj[i]];
} else if(_isObject(obj[i]) && '@id' in obj[i]) {
obj[i] = top[obj[i]['@id']];
}
recurse(obj[i]);
}
} else if(_isObject(obj)) {
var sid = obj['@id'];
subject[k] = obj = top[sid];
recurse(obj);
}
}
};
recurse.visited = {};
recurse(top);
compacted.of_type = {};
for(var s in top) {
if(!('@type' in top[s])) {
continue;
}
var types = top[s]['@type'];
if(!_isArray(types)) {
types = [types];
}
for(var t in types) {
if(!(types[t] in compacted.of_type)) {
compacted.of_type[types[t]] = [];
}
compacted.of_type[types[t]].push(top[s]);
}
}
callback(null, compacted);
});
});
};
/**
* Performs RDF dataset normalization on the given JSON-LD input. The output
* is an RDF dataset unless the 'format' option is used.
*
* @param input the JSON-LD input to normalize.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [format] the format if output is a string:
* 'application/nquads' for N-Quads.
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, normalized) called once the operation completes.
*/
jsonld.normalize = function(input, options, callback) {
if(arguments.length < 1) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not normalize, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// set default options
if(!('base' in options)) {
options.base = (typeof input === 'string') ? input : '';
}
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
// convert to RDF dataset then do normalization
var opts = _clone(options);
delete opts.format;
opts.produceGeneralizedRdf = false;
jsonld.toRDF(input, opts, function(err, dataset) {
if(err) {
return callback(new JsonLdError(
'Could not convert input to RDF dataset before normalization.',
'jsonld.NormalizeError', {cause: err}));
}
// do normalization
new Processor().normalize(dataset, options, callback);
});
};
/**
* Converts an RDF dataset to JSON-LD.
*
* @param dataset a serialized string of RDF in a format specified by the
* format option or an RDF dataset to convert.
* @param [options] the options to use:
* [format] the format if dataset param must first be parsed:
* 'application/nquads' for N-Quads (default).
* [rdfParser] a custom RDF-parser to use to parse the dataset.
* [useRdfType] true to use rdf:type, false to use @type
* (default: false).
* [useNativeTypes] true to convert XSD types into native types
* (boolean, integer, double), false not to (default: false).
* @param callback(err, output) called once the operation completes.
*/
jsonld.fromRDF = function(dataset, options, callback) {
if(arguments.length < 1) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not convert from RDF, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// set default options
if(!('useRdfType' in options)) {
options.useRdfType = false;
}
if(!('useNativeTypes' in options)) {
options.useNativeTypes = false;
}
if(!('format' in options) && _isString(dataset)) {
// set default format to nquads
if(!('format' in options)) {
options.format = 'application/nquads';
}
}
jsonld.nextTick(function() {
// handle special format
var rdfParser;
if(options.format) {
// check supported formats
rdfParser = options.rdfParser || _rdfParsers[options.format];
if(!rdfParser) {
return callback(new JsonLdError(
'Unknown input format.',
'jsonld.UnknownFormat', {format: options.format}));
}
} else {
// no-op parser, assume dataset already parsed
rdfParser = function() {
return dataset;
};
}
// rdf parser may be async or sync, always pass callback
dataset = rdfParser(dataset, function(err, dataset) {
if(err) {
return callback(err);
}
fromRDF(dataset, options, callback);
});
// handle synchronous or promise-based parser
if(dataset) {
// if dataset is actually a promise
if('then' in dataset) {
return dataset.then(function(dataset) {
fromRDF(dataset, options, callback);
}, callback);
}
// parser is synchronous
fromRDF(dataset, options, callback);
}
function fromRDF(dataset, options, callback) {
// convert from RDF
new Processor().fromRDF(dataset, options, callback);
}
});
};
/**
* Outputs the RDF dataset found in the given JSON-LD object.
*
* @param input the JSON-LD input.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [format] the format to use to output a string:
* 'application/nquads' for N-Quads.
* [produceGeneralizedRdf] true to output generalized RDF, false
* to produce only standard RDF (default: false).
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, dataset) called once the operation completes.
*/
jsonld.toRDF = function(input, options, callback) {
if(arguments.length < 1) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not convert to RDF, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// set default options
if(!('base' in options)) {
options.base = (typeof input === 'string') ? input : '';
}
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
// expand input
jsonld.expand(input, options, function(err, expanded) {
if(err) {
return callback(new JsonLdError(
'Could not expand input before serialization to RDF.',
'jsonld.RdfError', {cause: err}));
}
var dataset;
try {
// output RDF dataset
dataset = Processor.prototype.toRDF(expanded, options);
if(options.format) {
if(options.format === 'application/nquads') {
return callback(null, _toNQuads(dataset));
}
throw new JsonLdError(
'Unknown output format.',
'jsonld.UnknownFormat', {format: options.format});
}
} catch(ex) {
return callback(ex);
}
callback(null, dataset);
});
};
/**
* **Experimental**
*
* Recursively flattens the nodes in the given JSON-LD input into a map of
* node ID => node.
*
* @param input the JSON-LD input.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [namer] a jsonld.UniqueNamer to use to label blank nodes.
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, nodeMap) called once the operation completes.
*/
jsonld.createNodeMap = function(input, options, callback) {
if(arguments.length < 1) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not create node map, too few arguments.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// set default options
if(!('base' in options)) {
options.base = (typeof input === 'string') ? input : '';
}
if(!('documentLoader' in options)) {
options.documentLoader = jsonld.loadDocument;
}
// expand input
jsonld.expand(input, options, function(err, _input) {
if(err) {
return callback(new JsonLdError(
'Could not expand input before creating node map.',
'jsonld.CreateNodeMapError', {cause: err}));
}
var nodeMap;
try {
nodeMap = new Processor().createNodeMap(_input, options);
} catch(ex) {
return callback(ex);
}
callback(null, nodeMap);
});
};
/**
* **Experimental**
*
* Merges two or more JSON-LD documents into a single flattened document.
*
* @param docs the JSON-LD documents to merge together.
* @param ctx the context to use to compact the merged result, or null.
* @param [options] the options to use:
* [base] the base IRI to use.
* [expandContext] a context to expand with.
* [namer] a jsonld.UniqueNamer to use to label blank nodes.
* [mergeNodes] true to merge properties for nodes with the same ID,
* false to ignore new properties for nodes with the same ID once
* the ID has been defined; note that this may not prevent merging
* new properties where a node is in the `object` position
* (default: true).
* [documentLoader(url, callback(err, remoteDoc))] the document loader.
* @param callback(err, merged) called once the operation completes.
*/
jsonld.merge = function(docs, ctx, options, callback) {
if(arguments.length < 1) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not merge, too few arguments.'));
});
}
if(!_isArray(docs)) {
return jsonld.nextTick(function() {
callback(new TypeError('Could not merge, "docs" must be an array.'));
});
}
// get arguments
if(typeof options === 'function') {
callback = options;
options = {};
} else if(typeof ctx === 'function') {
callback = ctx;
ctx = null;
options = {};
}
options = options || {};
// expand all documents
var expanded = [];
var error = null;
var count = docs.length;
for(var i = 0; i < docs.length; ++i) {
var opts = {};
for(var key in options) {
opts[key] = options[key];
}
jsonld.expand(docs[i], opts, expandComplete);
}
function expandComplete(err, _input) {
if(error) {
return;
}
if(err) {
error = err;
return callback(new JsonLdError(
'Could not expand input before flattening.',
'jsonld.FlattenError', {cause: err}));
}
expanded.push(_input);
if(--count === 0) {
merge(expanded);
}
}
function merge(expanded) {
var mergeNodes = true;
if('mergeNodes' in options) {
mergeNodes = options.mergeNodes;
}
var namer = options.namer || new UniqueNamer('_:b');
var graphs = {'@default': {}};
var defaultGraph;
try {
for(var i = 0; i < expanded.length; ++i) {
// uniquely relabel blank nodes
var doc = expanded[i];
doc = jsonld.relabelBlankNodes(doc, {
namer: new UniqueNamer('_:b' + i + '-')
});
// add nodes to the shared node map graphs if merging nodes, to a
// separate graph set if not
var _graphs = (mergeNodes || i === 0) ? graphs : {'@default': {}};
_createNodeMap(doc, _graphs, '@default', namer);
if(_graphs !== graphs) {
// merge document graphs but don't merge existing nodes
for(var graphName in _graphs) {
var _nodeMap = _graphs[graphName];
if(!(graphName in graphs)) {
graphs[graphName] = _nodeMap;
continue;
}
var nodeMap = graphs[graphName];
for(var key in _nodeMap) {
if(!(key in nodeMap)) {
nodeMap[key] = _nodeMap[key];
}
}
}
}
}
// add all non-default graphs to default graph
defaultGraph = _mergeNodeMaps(graphs);
} catch(ex) {
return callback(ex);
}
// produce flattened output
var flattened = [];
var keys = Object.keys(defaultGraph).sort();
for(var ki = 0; ki < keys.length; ++ki) {
var node = defaultGraph[keys[ki]];
// only add full subjects to top-level
if(!_isSubjectReference(node)) {
flattened.push(node);
}
}
if(ctx === null) {
return callback(null, flattened);
}
// compact result (force @graph option to true, skip expansion)
options.graph = true;
options.skipExpansion = true;
jsonld.compact(flattened, ctx, options, function(err, compacted) {
if(err) {
return callback(new JsonLdError(
'Could not compact merged output.',
'jsonld.MergeError', {cause: err}));
}
callback(null, compacted);
});
}
};
/**
* Relabels all blank nodes in the given JSON-LD input.
*
* @param input the JSON-LD input.
* @param [options] the options to use:
* [namer] a jsonld.UniqueNamer to use.
*/
jsonld.relabelBlankNodes = function(input, options) {
options = options || {};
var namer = options.namer || new UniqueNamer('_:b');
return _labelBlankNodes(namer, input);
};
/**
* The default document loader for external documents. If the environment
* is node.js, a callback-continuation-style document loader is used; otherwise,
* a promises-style document loader is used.
*
* @param url the URL to load.
* @param callback(err, remoteDoc) called once the operation completes,
* if using a non-promises API.
*
* @return a promise, if using a promises API.
*/
jsonld.documentLoader = function(url, callback) {
var err = new JsonLdError(
'Could not retrieve a JSON-LD document from the URL. URL ' +
'dereferencing not implemented.', 'jsonld.LoadDocumentError',
{code: 'loading document failed'});
if(_nodejs) {
return callback(err, {contextUrl: null, documentUrl: url, document: null});
}
return jsonld.promisify(function(callback) {
callback(err);
});
};
/**
* Deprecated default document loader. Use or override jsonld.documentLoader
* instead.
*/
jsonld.loadDocument = function(url, callback) {
var promise = jsonld.documentLoader(url, callback);
if(promise && 'then' in promise) {
promise.then(callback.bind(null, null), callback);
}
};
/* Promises API */
/**
* Creates a new promises API object.
*
* @param [options] the options to use:
* [api] an object to attach the API to.
* [version] 'json-ld-1.0' to output a standard JSON-LD 1.0 promises
* API, 'jsonld.js' to output the same with augmented proprietary
* methods (default: 'jsonld.js')
*
* @return the promises API object.
*/
jsonld.promises = function(options) {
options = options || {};
var slice = Array.prototype.slice;
var promisify = jsonld.promisify;
// handle 'api' option as version, set defaults
var api = options.api || {};
var version = options.version || 'jsonld.js';
if(typeof options.api === 'string') {
if(!options.version) {
version = options.api;
}
api = {};
}
api.expand = function(input) {
if(arguments.length < 1) {
throw new TypeError('Could not expand, too few arguments.');
}
return promisify.apply(null, [jsonld.expand].concat(slice.call(arguments)));
};
api.compact = function(input, ctx) {
if(arguments.length < 2) {
throw new TypeError('Could not compact, too few arguments.');
}
var compact = function(input, ctx, options, callback) {
// ensure only one value is returned in callback
jsonld.compact(input, ctx, options, function(err, compacted) {
callback(err, compacted);
});
};
return promisify.apply(null, [compact].concat(slice.call(arguments)));
};
api.flatten = function(input) {
if(arguments.length < 1) {
throw new TypeError('Could not flatten, too few arguments.');
}
return promisify.apply(
null, [jsonld.flatten].concat(slice.call(arguments)));
};
api.frame = function(input, frame) {
if(arguments.length < 2) {
throw new TypeError('Could not frame, too few arguments.');
}
return promisify.apply(null, [jsonld.frame].concat(slice.call(arguments)));
};
api.fromRDF = function(dataset) {
if(arguments.length < 1) {
throw new TypeError('Could not convert from RDF, too few arguments.');
}
return promisify.apply(
null, [jsonld.fromRDF].concat(slice.call(arguments)));
};
api.toRDF = function(input) {
if(arguments.length < 1) {
throw new TypeError('Could not convert to RDF, too few arguments.');
}
return promisify.apply(null, [jsonld.toRDF].concat(slice.call(arguments)));
};
api.normalize = function(input) {
if(arguments.length < 1) {
throw new TypeError('Could not normalize, too few arguments.');
}
return promisify.apply(
null, [jsonld.normalize].concat(slice.call(arguments)));
};
if(version === 'jsonld.js') {
api.link = function(input, ctx) {
if(arguments.length < 2) {
throw new TypeError('Could not link, too few arguments.');
}
return promisify.apply(
null, [jsonld.link].concat(slice.call(arguments)));
};
api.objectify = function(input) {
return promisify.apply(
null, [jsonld.objectify].concat(slice.call(arguments)));
};
api.createNodeMap = function(input) {
return promisify.apply(
null, [jsonld.createNodeMap].concat(slice.call(arguments)));
};
api.merge = function(input) {
return promisify.apply(
null, [jsonld.merge].concat(slice.call(arguments)));
};
}
try {
jsonld.Promise = global.Promise || require('es6-promise').Promise;
} catch(e) {
var f = function() {
throw new Error('Unable to find a Promise implementation.');
};
for(var method in api) {
api[method] = f;
}
}
return api;
};
/**
* Converts a node.js async op into a promise w/boxed resolved value(s).
*
* @param op the operation to convert.
*
* @return the promise.
*/
jsonld.promisify = function(op) {
if(!jsonld.Promise) {
try {
jsonld.Promise = global.Promise || require('es6-promise').Promise;
} catch(e) {
throw new Error('Unable to find a Promise implementation.');
}
}
var args = Array.prototype.slice.call(arguments, 1);
return new jsonld.Promise(function(resolve, reject) {
op.apply(null, args.concat(function(err, value) {
if(!err) {
resolve(value);
} else {
reject(err);
}
}));
});
};
// extend jsonld.promises w/jsonld.js methods
jsonld.promises({api: jsonld.promises});
/* WebIDL API */
function JsonLdProcessor() {}
JsonLdProcessor.prototype = jsonld.promises({version: 'json-ld-1.0'});
JsonLdProcessor.prototype.toString = function() {
if(this instanceof JsonLdProcessor) {
return '[object JsonLdProcessor]';
}
return '[object JsonLdProcessorPrototype]';
};
jsonld.JsonLdProcessor = JsonLdProcessor;
// IE8 has Object.defineProperty but it only
// works on DOM nodes -- so feature detection
// requires try/catch :-(
var canDefineProperty = !!Object.defineProperty;
if(canDefineProperty) {
try {
Object.defineProperty({}, 'x', {});
} catch(e) {
canDefineProperty = false;
}
}
if(canDefineProperty) {
Object.defineProperty(JsonLdProcessor, 'prototype', {
writable: false,
enumerable: false
});
Object.defineProperty(JsonLdProcessor.prototype, 'constructor', {
writable: true,
enumerable: false,
configurable: true,
value: JsonLdProcessor
});
}
// setup browser global JsonLdProcessor
if(_browser && typeof global.JsonLdProcessor === 'undefined') {
if(canDefineProperty) {
Object.defineProperty(global, 'JsonLdProcessor', {
writable: true,
enumerable: false,
configurable: true,
value: JsonLdProcessor
});
} else {
global.JsonLdProcessor = JsonLdProcessor;
}
}
/* Utility API */
// define setImmediate and nextTick
if(typeof process === 'undefined' || !process.nextTick) {
if(typeof setImmediate === 'function') {
jsonld.setImmediate = jsonld.nextTick = function(callback) {
return setImmediate(callback);
};
} else {
jsonld.setImmediate = function(callback) {
setTimeout(callback, 0);
};
jsonld.nextTick = jsonld.setImmediate;
}
} else {
jsonld.nextTick = process.nextTick;
if(typeof setImmediate === 'function') {
jsonld.setImmediate = setImmediate;
} else {
jsonld.setImmediate = jsonld.nextTick;
}
}
/**
* Parses a link header. The results will be key'd by the value of "rel".
*
* Link: <http://json-ld.org/contexts/person.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
*
* Parses as: {
* 'http://www.w3.org/ns/json-ld#context': {
* target: http://json-ld.org/contexts/person.jsonld,
* type: 'application/ld+json'
* }
* }
*
* If there is more than one "rel" with the same IRI, then entries in the
* resulting map for that "rel" will be arrays.
*
* @param header the link header to parse.
*/
jsonld.parseLinkHeader = function(header) {
var rval = {};
// split on unbracketed/unquoted commas
var entries = header.match(/(?:<[^>]*?>|"[^"]*?"|[^,])+/g);
var rLinkHeader = /\s*<([^>]*?)>\s*(?:;\s*(.*))?/;
for(var i = 0; i < entries.length; ++i) {
var match = entries[i].match(rLinkHeader);
if(!match) {
continue;
}
var result = {target: match[1]};
var params = match[2];
var rParams = /(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/g;
while(match = rParams.exec(params)) {
result[match[1]] = (match[2] === undefined) ? match[3] : match[2];
}
var rel = result['rel'] || '';
if(_isArray(rval[rel])) {
rval[rel].push(result);
} else if(rel in rval) {
rval[rel] = [rval[rel], result];
} else {
rval[rel] = result;
}
}
return rval;
};
/**
* Creates a simple document cache that retains documents for a short
* period of time.
*
* FIXME: Implement simple HTTP caching instead.
*
* @param size the maximum size of the cache.
*/
jsonld.DocumentCache = function(size) {
this.order = [];
this.cache = {};
this.size = size || 50;
this.expires = 30 * 1000;
};
jsonld.DocumentCache.prototype.get = function(url) {
if(url in this.cache) {
var entry = this.cache[url];
if(entry.expires >= +new Date()) {
return entry.ctx;
}
delete this.cache[url];
this.order.splice(this.order.indexOf(url), 1);
}
return null;
};
jsonld.DocumentCache.prototype.set = function(url, ctx) {
if(this.order.length === this.size) {
delete this.cache[this.order.shift()];
}
this.order.push(url);
this.cache[url] = {ctx: ctx, expires: (+new Date() + this.expires)};
};
/**
* Creates an active context cache.
*
* @param size the maximum size of the cache.
*/
jsonld.ActiveContextCache = function(size) {
this.order = [];
this.cache = {};
this.size = size || 100;
};
jsonld.ActiveContextCache.prototype.get = function(activeCtx, localCtx) {
var key1 = JSON.stringify(activeCtx);
var key2 = JSON.stringify(localCtx);
var level1 = this.cache[key1];
if(level1 && key2 in level1) {
return level1[key2];
}
return null;
};
jsonld.ActiveContextCache.prototype.set = function(
activeCtx, localCtx, result) {
if(this.order.length === this.size) {
var entry = this.order.shift();
delete this.cache[entry.activeCtx][entry.localCtx];
}
var key1 = JSON.stringify(activeCtx);
var key2 = JSON.stringify(localCtx);
this.order.push({activeCtx: key1, localCtx: key2});
if(!(key1 in this.cache)) {
this.cache[key1] = {};
}
this.cache[key1][key2] = _clone(result);
};
/**
* Default JSON-LD cache.
*/
jsonld.cache = {
activeCtx: new jsonld.ActiveContextCache()
};
/**
* Document loaders.
*/
jsonld.documentLoaders = {};
/**
* Creates a built-in jquery document loader.
*
* @param $ the jquery instance to use.
* @param options the options to use:
* secure: require all URLs to use HTTPS.
* usePromise: true to use a promises API, false for a
* callback-continuation-style API; defaults to true if Promise
* is globally defined, false if not.
*
* @return the jquery document loader.
*/
jsonld.documentLoaders.jquery = function($, options) {
options = options || {};
var loader = function(url, callback) {
if(url.indexOf('http:') !== 0 && url.indexOf('https:') !== 0) {
return callback(new JsonLdError(
'URL could not be dereferenced; only "http" and "https" URLs are ' +
'supported.',
'jsonld.InvalidUrl', {code: 'loading document failed', url: url}),
{contextUrl: null, documentUrl: url, document: null});
}
if(options.secure && url.indexOf('https') !== 0) {
return callback(new JsonLdError(
'URL could not be dereferenced; secure mode is enabled and ' +
'the URL\'s scheme is not "https".',
'jsonld.InvalidUrl', {code: 'loading document failed', url: url}),
{contextUrl: null, documentUrl: url, document: null});
}
$.ajax({
url: url,
accepts: {
json: 'application/ld+json, application/json'
},
// ensure Accept header is very specific for JSON-LD/JSON
headers: {
'Accept': 'application/ld+json, application/json'
},
dataType: 'json',
crossDomain: true,
success: function(data, textStatus, jqXHR) {
var doc = {contextUrl: null, documentUrl: url, document: data};
// handle Link Header
var contentType = jqXHR.getResponseHeader('Content-Type');
var linkHeader = jqXHR.getResponseHeader('Link');
if(linkHeader && contentType !== 'application/ld+json') {
// only 1 related link header permitted
linkHeader = jsonld.parseLinkHeader(linkHeader)[LINK_HEADER_REL];
if(_isArray(linkHeader)) {
return callback(new JsonLdError(
'URL could not be dereferenced, it has more than one ' +
'associated HTTP Link Header.',
'jsonld.InvalidUrl',
{code: 'multiple context link headers', url: url}), doc);
}
if(linkHeader) {
doc.contextUrl = linkHeader.target;
}
}
callback(null, doc);
},
error: function(jqXHR, textStatus, err) {
callback(new JsonLdError(
'URL could not be dereferenced, an error occurred.',
'jsonld.LoadDocumentError',
{code: 'loading document failed', url: url, cause: err}),
{contextUrl: null, documentUrl: url, document: null});
}
});
};
var usePromise = (typeof Promise !== 'undefined');
if('usePromise' in options) {
usePromise = options.usePromise;
}
if(usePromise) {
return function(url) {
return jsonld.promisify(loader, url);
};
}
return loader;
};
/**
* Creates a built-in node document loader.
*
* @param options the options to use:
* secure: require all URLs to use HTTPS.
* strictSSL: true to require SSL certificates to be valid,
* false not to (default: true).
* maxRedirects: the maximum number of redirects to permit, none by
* default.
* usePromise: true to use a promises API, false for a
* callback-continuation-style API; false by default.
*
* @return the node document loader.
*/
jsonld.documentLoaders.node = function(options) {
options = options || {};
var strictSSL = ('strictSSL' in options) ? options.strictSSL : true;
var maxRedirects = ('maxRedirects' in options) ? options.maxRedirects : -1;
var request = require('request');
var http = require('http');
var cache = new jsonld.DocumentCache();
function loadDocument(url, redirects, callback) {
if(url.indexOf('http:') !== 0 && url.indexOf('https:') !== 0) {
return callback(new JsonLdError(
'URL could not be dereferenced; only "http" and "https" URLs are ' +
'supported.',
'jsonld.InvalidUrl', {code: 'loading document failed', url: u