/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const {isKeyword} = require('./context'); const graphTypes = require('./graphTypes'); const types = require('./types'); const util = require('./util'); const JsonLdError = require('./JsonLdError'); const api = {}; module.exports = api; /** * Creates a merged JSON-LD node map (node ID => node). * * @param input the expanded JSON-LD to create a node map of. * @param [options] the options to use: * [issuer] a jsonld.IdentifierIssuer to use to label blank nodes. * * @return the node map. */ api.createMergedNodeMap = (input, options) => { options = options || {}; // produce a map of all subjects and name each bnode const issuer = options.issuer || new util.IdentifierIssuer('_:b'); const graphs = {'@default': {}}; api.createNodeMap(input, graphs, '@default', issuer); // add all non-default graphs to default graph return api.mergeNodeMaps(graphs); }; /** * 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 issuer the blank node identifier issuer. * @param name the name assigned to the current input if it is a bnode. * @param list the list to append to, null for none. */ api.createNodeMap = (input, graphs, graph, issuer, name, list) => { // recurse through array if(types.isArray(input)) { for(const node of input) { api.createNodeMap(node, graphs, graph, issuer, undefined, list); } return; } // add non-object to list if(!types.isObject(input)) { if(list) { list.push(input); } return; } // add values to list if(graphTypes.isValue(input)) { if('@type' in input) { let type = input['@type']; // rename @type blank node if(type.indexOf('_:') === 0) { input['@type'] = type = issuer.getId(type); } } if(list) { list.push(input); } return; } else if(list && graphTypes.isList(input)) { const _list = []; api.createNodeMap(input['@list'], graphs, graph, issuer, name, _list); list.push({'@list': _list}); return; } // Note: At this point, input must be a subject. // spec requires @type to be named first, so assign names early if('@type' in input) { const types = input['@type']; for(const type of types) { if(type.indexOf('_:') === 0) { issuer.getId(type); } } } // get name for subject if(types.isUndefined(name)) { name = graphTypes.isBlankNode(input) ? issuer.getId(input['@id']) : input['@id']; } // add subject reference to list if(list) { list.push({'@id': name}); } // create new subject or merge into existing one const subjects = graphs[graph]; const subject = subjects[name] = subjects[name] || {}; subject['@id'] = name; const properties = Object.keys(input).sort(); for(let property of properties) { // skip @id if(property === '@id') { continue; } // handle reverse properties if(property === '@reverse') { const referencedNode = {'@id': name}; const reverseMap = input['@reverse']; for(const reverseProperty in reverseMap) { const items = reverseMap[reverseProperty]; for(const item of items) { let itemName = item['@id']; if(graphTypes.isBlankNode(item)) { itemName = issuer.getId(itemName); } api.createNodeMap(item, graphs, graph, issuer, itemName); util.addValue( subjects[itemName], reverseProperty, referencedNode, {propertyIsArray: true, allowDuplicate: false}); } } continue; } // recurse into graph if(property === '@graph') { // add graph subjects map entry if(!(name in graphs)) { graphs[name] = {}; } api.createNodeMap(input[property], graphs, name, issuer); continue; } // recurse into included if(property === '@included') { api.createNodeMap(input[property], graphs, graph, issuer); continue; } // copy non-@type keywords if(property !== '@type' && isKeyword(property)) { if(property === '@index' && property in subject && (input[property] !== subject[property] || input[property]['@id'] !== subject[property]['@id'])) { throw new JsonLdError( 'Invalid JSON-LD syntax; conflicting @index property detected.', 'jsonld.SyntaxError', {code: 'conflicting indexes', subject}); } subject[property] = input[property]; continue; } // iterate over objects const objects = input[property]; // if property is a bnode, assign it a new id if(property.indexOf('_:') === 0) { property = issuer.getId(property); } // ensure property is added for empty arrays if(objects.length === 0) { util.addValue(subject, property, [], {propertyIsArray: true}); continue; } for(let o of objects) { if(property === '@type') { // rename @type blank nodes o = (o.indexOf('_:') === 0) ? issuer.getId(o) : o; } // handle embedded subject or subject reference if(graphTypes.isSubject(o) || graphTypes.isSubjectReference(o)) { // skip null @id if('@id' in o && !o['@id']) { continue; } // relabel blank node @id const id = graphTypes.isBlankNode(o) ? issuer.getId(o['@id']) : o['@id']; // add reference and recurse util.addValue( subject, property, {'@id': id}, {propertyIsArray: true, allowDuplicate: false}); api.createNodeMap(o, graphs, graph, issuer, id); } else if(graphTypes.isValue(o)) { util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); } else if(graphTypes.isList(o)) { // handle @list const _list = []; api.createNodeMap(o['@list'], graphs, graph, issuer, name, _list); o = {'@list': _list}; util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); } else { // handle @value api.createNodeMap(o, graphs, graph, issuer, name); util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); } } } }; /** * Merge separate named graphs into a single merged graph including * all nodes from the default graph and named graphs. * * @param graphs a map of graph name to subject map. * * @return the merged graph map. */ api.mergeNodeMapGraphs = graphs => { const merged = {}; for(const name of Object.keys(graphs).sort()) { for(const id of Object.keys(graphs[name]).sort()) { const node = graphs[name][id]; if(!(id in merged)) { merged[id] = {'@id': id}; } const mergedNode = merged[id]; for(const property of Object.keys(node).sort()) { if(isKeyword(property) && property !== '@type') { // copy keywords mergedNode[property] = util.clone(node[property]); } else { // merge objects for(const value of node[property]) { util.addValue( mergedNode, property, util.clone(value), {propertyIsArray: true, allowDuplicate: false}); } } } } } return merged; }; api.mergeNodeMaps = graphs => { // add all non-default graphs to default graph const defaultGraph = graphs['@default']; const graphNames = Object.keys(graphs).sort(); for(const graphName of graphNames) { if(graphName === '@default') { continue; } const nodeMap = graphs[graphName]; let subject = defaultGraph[graphName]; if(!subject) { defaultGraph[graphName] = subject = { '@id': graphName, '@graph': [] }; } else if(!('@graph' in subject)) { subject['@graph'] = []; } const graph = subject['@graph']; for(const id of Object.keys(nodeMap).sort()) { const node = nodeMap[id]; // only add full subjects if(!graphTypes.isSubjectReference(node)) { graph.push(node); } } } return defaultGraph; };