jsonld
Version:
A JSON-LD Processor and API implementation in JavaScript.
264 lines (234 loc) • 7.58 kB
JavaScript
/*
* Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved.
*/
;
const {
isArray: _isArray,
isObject: _isObject,
isString: _isString,
} = require('./types');
const {
asArray: _asArray
} = require('./util');
const {prependBase} = require('./url');
const JsonLdError = require('./JsonLdError');
const ResolvedContext = require('./ResolvedContext');
const MAX_CONTEXT_URLS = 10;
module.exports = class ContextResolver {
/**
* Creates a ContextResolver.
*
* @param sharedCache a shared LRU cache with `get` and `set` APIs.
*/
constructor({sharedCache}) {
this.perOpCache = new Map();
this.sharedCache = sharedCache;
}
async resolve({
activeCtx, context, documentLoader, base, cycles = new Set()
}) {
// process `@context`
if(context && _isObject(context) && context['@context']) {
context = context['@context'];
}
// context is one or more contexts
context = _asArray(context);
// resolve each context in the array
const allResolved = [];
for(const ctx of context) {
if(_isString(ctx)) {
// see if `ctx` has been resolved before...
let resolved = this._get(ctx);
if(!resolved) {
// not resolved yet, resolve
resolved = await this._resolveRemoteContext(
{activeCtx, url: ctx, documentLoader, base, cycles});
}
// add to output and continue
if(_isArray(resolved)) {
allResolved.push(...resolved);
} else {
allResolved.push(resolved);
}
continue;
}
if(ctx === null) {
// handle `null` context, nothing to cache
allResolved.push(new ResolvedContext({document: null}));
continue;
}
if(!_isObject(ctx)) {
_throwInvalidLocalContext(context);
}
// context is an object, get/create `ResolvedContext` for it
const key = JSON.stringify(ctx);
let resolved = this._get(key);
if(!resolved) {
// create a new static `ResolvedContext` and cache it
resolved = new ResolvedContext({document: ctx});
this._cacheResolvedContext({key, resolved, tag: 'static'});
}
allResolved.push(resolved);
}
return allResolved;
}
_get(key) {
// get key from per operation cache; no `tag` is used with this cache so
// any retrieved context will always be the same during a single operation
let resolved = this.perOpCache.get(key);
if(!resolved) {
// see if the shared cache has a `static` entry for this URL
const tagMap = this.sharedCache.get(key);
if(tagMap) {
resolved = tagMap.get('static');
if(resolved) {
this.perOpCache.set(key, resolved);
}
}
}
return resolved;
}
_cacheResolvedContext({key, resolved, tag}) {
this.perOpCache.set(key, resolved);
if(tag !== undefined) {
let tagMap = this.sharedCache.get(key);
if(!tagMap) {
tagMap = new Map();
this.sharedCache.set(key, tagMap);
}
tagMap.set(tag, resolved);
}
return resolved;
}
async _resolveRemoteContext({activeCtx, url, documentLoader, base, cycles}) {
// resolve relative URL and fetch context
url = prependBase(base, url);
const {context, remoteDoc} = await this._fetchContext(
{activeCtx, url, documentLoader, cycles});
// update base according to remote document and resolve any relative URLs
base = remoteDoc.documentUrl || url;
_resolveContextUrls({context, base});
// resolve, cache, and return context
const resolved = await this.resolve(
{activeCtx, context, documentLoader, base, cycles});
this._cacheResolvedContext({key: url, resolved, tag: remoteDoc.tag});
return resolved;
}
async _fetchContext({activeCtx, url, documentLoader, cycles}) {
// check for max context URLs fetched during a resolve operation
if(cycles.size > MAX_CONTEXT_URLS) {
throw new JsonLdError(
'Maximum number of @context URLs exceeded.',
'jsonld.ContextUrlError',
{
code: activeCtx.processingMode === 'json-ld-1.0' ?
'loading remote context failed' :
'context overflow',
max: MAX_CONTEXT_URLS
});
}
// check for context URL cycle
// shortcut to avoid extra work that would eventually hit the max above
if(cycles.has(url)) {
throw new JsonLdError(
'Cyclical @context URLs detected.',
'jsonld.ContextUrlError',
{
code: activeCtx.processingMode === 'json-ld-1.0' ?
'recursive context inclusion' :
'context overflow',
url
});
}
// track cycles
cycles.add(url);
let context;
let remoteDoc;
try {
remoteDoc = await documentLoader(url);
context = remoteDoc.document || null;
// parse string context as JSON
if(_isString(context)) {
context = JSON.parse(context);
}
} catch(e) {
throw new JsonLdError(
'Dereferencing a URL did not result in a valid JSON-LD object. ' +
'Possible causes are an inaccessible URL perhaps due to ' +
'a same-origin policy (ensure the server uses CORS if you are ' +
'using client-side JavaScript), too many redirects, a ' +
'non-JSON response, or more than one HTTP Link Header was ' +
'provided for a remote context. ' +
`URL: "${url}".`,
'jsonld.InvalidUrl',
{code: 'loading remote context failed', url, cause: e});
}
// ensure ctx is an object
if(!_isObject(context)) {
throw new JsonLdError(
'Dereferencing a URL did not result in a JSON object. The ' +
'response was valid JSON, but it was not a JSON object. ' +
`URL: "${url}".`,
'jsonld.InvalidUrl', {code: 'invalid remote context', url});
}
// use empty context if no @context key is present
if(!('@context' in context)) {
context = {'@context': {}};
} else {
context = {'@context': context['@context']};
}
// append @context URL to context if given
if(remoteDoc.contextUrl) {
if(!_isArray(context['@context'])) {
context['@context'] = [context['@context']];
}
context['@context'].push(remoteDoc.contextUrl);
}
return {context, remoteDoc};
}
};
function _throwInvalidLocalContext(ctx) {
throw new JsonLdError(
'Invalid JSON-LD syntax; @context must be an object.',
'jsonld.SyntaxError', {
code: 'invalid local context', context: ctx
});
}
/**
* Resolve all relative `@context` URLs in the given context by inline
* replacing them with absolute URLs.
*
* @param context the context.
* @param base the base IRI to use to resolve relative IRIs.
*/
function _resolveContextUrls({context, base}) {
if(!context) {
return;
}
const ctx = context['@context'];
if(_isString(ctx)) {
context['@context'] = prependBase(base, ctx);
return;
}
if(_isArray(ctx)) {
for(let i = 0; i < ctx.length; ++i) {
const element = ctx[i];
if(_isString(element)) {
ctx[i] = prependBase(base, element);
continue;
}
if(_isObject(element)) {
_resolveContextUrls({context: {'@context': element}, base});
}
}
return;
}
if(!_isObject(ctx)) {
// no @context URLs can be found in non-object
return;
}
// ctx is an object, resolve any context URLs in terms
for(const term in ctx) {
_resolveContextUrls({context: ctx[term], base});
}
}