diff --git a/package-lock.json b/package-lock.json index ead45f5df..07491e873 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2033,6 +2033,11 @@ "integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==", "dev": true }, + "canonicalize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.1.tgz", + "integrity": "sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg==" + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -4976,10 +4981,11 @@ } }, "jsonld": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-1.6.2.tgz", - "integrity": "sha512-eMzFHqhF2kPMrMUjw8+Lz9IF1QkrxTOIfVndkP/OpuoZs31VdDtfDs8mLa5EOC/ROdemFTQGLdYPZbRtmMe2Yw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-1.8.0.tgz", + "integrity": "sha512-a3bwbR0wqFstxKsGoimUIIKBdfJ+yb9kWK+WK7MpVyvfYtITMpUtF3sNoN1wG/W+jGDgya0ACRh++jtTozxtyQ==", "requires": { + "canonicalize": "^1.0.1", "rdf-canonize": "^1.0.2", "request": "^2.88.0", "semver": "^5.6.0", @@ -6491,9 +6497,9 @@ "dev": true }, "psl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", - "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", + "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" }, "public-encrypt": { "version": "4.0.3", diff --git a/package.json b/package.json index f00cbba91..8db971223 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "async": "^3.1.x", - "jsonld": "^1.6.2", + "jsonld": "^1.8.0", "n3": "^1.2.0", "solid-auth-cli": "^1.0.8", "solid-auth-client": "^2.3.0", diff --git a/src/jsonldparser.js b/src/jsonldparser.js new file mode 100644 index 000000000..86c7bb358 --- /dev/null +++ b/src/jsonldparser.js @@ -0,0 +1,98 @@ +import jsonld from 'jsonld' + +import { arrayToStatements } from './util' + +/** + * Parses json-ld formatted JS objects to a rdf Term. + * @param kb - The DataFactory to use. + * @param obj - The json-ld object to process. + * @return {Literal|NamedNode|BlankNode|Collection} + */ +export function jsonldObjectToTerm (kb, obj) { + if (typeof obj === 'string') { + return kb.rdfFactory.literal(obj) + } + + if (Object.prototype.hasOwnProperty.call(obj, '@list')) { + if (kb.rdfFactory.supports["COLLECTIONS"] === true) { + return listToCollection(kb, obj['@list']) + } + + return listToStatements(kb, obj) + } + + if (Object.prototype.hasOwnProperty.call(obj, '@id')) { + return kb.rdfFactory.namedNode(obj['@id']) + } + + if (Object.prototype.hasOwnProperty.call(obj, '@language')) { + return kb.rdfFactory.literal(obj['@value'], obj['@language']) + } + + if (Object.prototype.hasOwnProperty.call(obj, '@type')) { + return kb.rdfFactory.literal(obj['@value'], kb.rdfFactory.namedNode(obj['@type'])) + } + + if (Object.prototype.hasOwnProperty.call(obj, '@value')) { + return kb.rdfFactory.literal(obj['@value']) + } + + return kb.rdfFactory.literal(obj) +} + +/** + * Adds the statements in a json-ld list object to {kb}. + */ +function listToStatements (kb, obj) { + const listId = obj['@id'] ? kb.rdfFactory.namedNode(obj['@id']) : kb.rdfFactory.blankNode() + + const items = obj['@list'].map((listItem => jsonldObjectToTerm(kb, listItem))) + const statements = arrayToStatements(kb.rdfFactory, listId, items) + kb.addAll(statements) + + return listId +} + +function listToCollection (kb, obj) { + if (!Array.isArray(obj)) { + throw new TypeError("Object must be an array") + } + return kb.rdfFactory.collection(obj.map((o) => jsonldObjectToTerm(kb, o))) +} + +/** + * Takes a json-ld formatted string {str} and adds its statements to {kb}. + * + * Ensure that {kb.rdfFactory} is a DataFactory. + */ +export default function jsonldParser (str, kb, base, callback) { + const baseString = Object.prototype.hasOwnProperty.call(base, 'termType') + ? base.value + : base + + return jsonld + .flatten(JSON.parse(str), null, { base: baseString }) + .then((flattened) => flattened.reduce((store, flatResource) => { + const id = flatResource['@id'] + ? kb.rdfFactory.namedNode(flatResource['@id']) + : kb.rdfFactory.blankNode() + + for (const property of Object.keys(flatResource)) { + if (property === '@id') { + continue + } + const value = flatResource[property] + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + kb.addStatement(kb.rdfFactory.quad(id, kb.rdfFactory.namedNode(property), jsonldObjectToTerm(kb, value[i]))) + } + } else { + kb.addStatement(kb.rdfFactory.quad(id, kb.rdfFactory.namedNode(property), jsonldObjectToTerm(kb, value))) + } + } + + return kb + }, kb)) + .then(callback) + .catch(callback) +} diff --git a/src/parse.js b/src/parse.js index 1279d2afa..ae6c47012 100644 --- a/src/parse.js +++ b/src/parse.js @@ -1,10 +1,7 @@ -import BlankNode from './blank-node' import DataFactory from './data-factory' -import jsonld from 'jsonld' -import Literal from './literal' +import jsonldParser from './jsonldparser' import { Parser as N3jsParser } from 'n3' // @@ Goal: remove this dependency import N3Parser from './n3parser' -import NamedNode from './named-node' import { parseRDFaDOM } from './rdfaparser' import RDFParser from './rdfxmlparser' import sparqlUpdateParser from './patch-parser' @@ -37,24 +34,12 @@ export default function parse (str, kb, base, contentType, callback) { } else if (contentType === 'application/sparql-update') { // @@ we handle a subset sparqlUpdateParser(str, kb, base) executeCallback() - } else if (contentType === 'application/ld+json' || - contentType === 'application/nquads' || + } else if (contentType === 'application/ld+json') { + jsonldParser(str, kb, base, executeCallback) + } else if (contentType === 'application/nquads' || contentType === 'application/n-quads') { var n3Parser = new N3jsParser({ factory: DataFactory }) - var triples = [] - if (contentType === 'application/ld+json') { - var jsonDocument - try { - jsonDocument = JSON.parse(str) - } catch (parseErr) { - return callback(parseErr, null) - } - jsonld.toRDF(jsonDocument, - {format: 'application/nquads', base}, - nquadCallback) - } else { - nquadCallback(null, str) - } + nquadCallback(null, str) } else { throw new Error("Don't know how to parse " + contentType + ' yet') } diff --git a/src/util.js b/src/util.js index aceeee4e5..c39de6519 100644 --- a/src/util.js +++ b/src/util.js @@ -2,6 +2,7 @@ * Utility functions for $rdf * @module util */ +import { jsonldObjectToTerm } from './jsonldparser' import { docpart } from './uri' import log from './log' import * as uri from './uri' @@ -69,6 +70,39 @@ export function AJAR_handleNewTerm (kb, p, requestedBy) { return sf.fetch(docuri, { referringTerm: requestedBy }) } +const rdf = { + first: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', + rest: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', + nil: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' +} + +/** + * Expands an array of Terms to a set of statements representing the rdf:list. + * @param rdfFactory - The factory to use + * @param {NamedNode|BlankNode} subject - The iri of the first list item. + * @param {Term[]} data - The terms to expand into the list. + * @return {Statement[]} - The {data} as a set of statements. + */ +export function arrayToStatements(rdfFactory, subject, data) { + const statements = [] + + data.reduce((id, listObj, i, listData) => { + statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.first), listData[i])) + + let nextNode + if (i < listData.length - 1) { + nextNode = rdfFactory.blankNode() + statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.rest), nextNode)) + } else { + statements.push(rdfFactory.quad(id, rdfFactory.namedNode(rdf.rest), rdfFactory.namedNode(rdf.nil))) + } + + return nextNode + }, subject) + + return statements +} + export function ArrayIndexOf (arr, item, i) { i || (i = 0) var length = arr.length diff --git a/tests/unit/parse-test.js b/tests/unit/parse-test.js index c5ae2dca6..0126f0944 100644 --- a/tests/unit/parse-test.js +++ b/tests/unit/parse-test.js @@ -2,7 +2,10 @@ import { expect } from 'chai' import parse from '../../src/parse' +import CanonicalDataFactory from '../../src/data-factory-internal' import DataFactory from '../../src/data-factory' +import Node from '../../src/node' +import defaultXSD from '../../src/xsd' describe('Parse', () => { describe('ttl', () => { @@ -41,20 +44,154 @@ describe('Parse', () => { "homepage": { "@id": "http://xmlns.com/foaf/0.1/homepage", "@type": "@id" - } + }, + "name": { + "@id": "http://xmlns.com/foaf/0.1/name", + "@container": "@language" + }, + "height": { + "@id": "http://schema.org/height", + "@type": "xsd:float" + }, + "list": { + "@id": "https://example.org/ns#listProp", + "@container": "@list" + }, + "xsd": "http://www.w3.org/2001/XMLSchema#" }, "@id": "../#me", - "homepage": "xyz" + "homepage": "xyz", + "name": { + "en": "The Queen", + "de": [ "Die Königin", "Ihre Majestät" ] + }, + "height": "173.9", + "list": [ + "list item 0", + "list item 1", + "list item 2" + ] + }` + store = DataFactory.graph(undefined, { rdfFactory: CanonicalDataFactory }) + parse(content, store, base, mimeType, done) + }) + + it('uses the specified base IRI', () => { + expect(store.rdfFactory.supports["COLLECTIONS"]).to.be.false + const homePageHeight = 5 // homepage + height + 3 x name + const list = 2 * 3 + 1 // (rdf:first + rdf:rest) * 3 items + listProp + expect(store.statements).to.have.length(homePageHeight + list); + + const height = store.statements[0] + expect(height.subject.value).to.equal('https://www.example.org/#me') + expect(height.predicate.value).to.equal('http://schema.org/height') + expect(height.object.datatype.value).to.equal('http://www.w3.org/2001/XMLSchema#float') + expect(height.object.value).to.equal('173.9') + + const homepage = store.statements[1] + expect(homepage.subject.value).to.equal('https://www.example.org/#me') + expect(homepage.predicate.value).to.equal('http://xmlns.com/foaf/0.1/homepage') + expect(homepage.object.value).to.equal('https://www.example.org/abc/xyz') + + const nameDe1 = store.statements[2] + expect(nameDe1.subject.value).to.equal('https://www.example.org/#me') + expect(nameDe1.predicate.value).to.equal('http://xmlns.com/foaf/0.1/name') + expect(nameDe1.object.value).to.equal('Die Königin') + + const nameDe2 = store.statements[3] + expect(nameDe2.subject.value).to.equal('https://www.example.org/#me') + expect(nameDe2.predicate.value).to.equal('http://xmlns.com/foaf/0.1/name') + expect(nameDe2.object.value).to.equal('Ihre Majestät') + + const nameEn = store.statements[4] + expect(nameEn.subject.value).to.equal('https://www.example.org/#me') + expect(nameEn.predicate.value).to.equal('http://xmlns.com/foaf/0.1/name') + expect(nameEn.object.value).to.equal('The Queen') + + const list0First = store.statements[5] + expect(list0First.subject.value).to.equal('n1') + expect(list0First.predicate.value).to.equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#first') + expect(list0First.object.value).to.equal('list item 0') + + const list0Rest = store.statements[6] + expect(list0Rest.subject.value).to.equal('n1') + expect(list0Rest.predicate.value).to.equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest') + expect(list0Rest.object.value).to.equal(store.statements[7].subject.value) + + const list1First = store.statements[7] + expect(list1First.subject.value).to.equal('n2') + expect(list1First.predicate.value).to.equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#first') + expect(list1First.object.value).to.equal('list item 1') + + const list1Rest = store.statements[8] + expect(list1Rest.subject.value).to.equal('n2') + expect(list1Rest.predicate.value).to.equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest') + expect(list1Rest.object.value).to.equal(store.statements[9].subject.value) + + const list2First = store.statements[9] + expect(list2First.subject.value).to.equal('n3') + expect(list2First.predicate.value).to.equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#first') + expect(list2First.object.value).to.equal('list item 2') + + const list2Rest = store.statements[10] + expect(list2Rest.subject.value).to.equal('n3') + expect(list2Rest.predicate.value).to.equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest') + expect(list2Rest.object.value).to.equal('http://www.w3.org/1999/02/22-rdf-syntax-ns#nil') + + const listProp = store.statements[11] + expect(listProp.subject.value).to.equal('https://www.example.org/#me') + expect(listProp.predicate.value).to.equal('https://example.org/ns#listProp') + expect(listProp.object.value).to.equal('n1') + }) + }) + + describe('with collections enabled', () => { + let store + before(done => { + const base = 'https://www.example.org/abc/def' + const mimeType = 'application/ld+json' + const content = ` + { + "@context": { + "list": { + "@id": "https://example.org/ns#listProp", + "@container": "@list" + }, + "xsd": "http://www.w3.org/2001/XMLSchema#" + }, + "@id": "../#me", + "list": [ + "list item 0", + 1, + { "@id": "http://example.com/2" } + ] }` store = DataFactory.graph() parse(content, store, base, mimeType, done) }) it('uses the specified base IRI', () => { + expect(store.rdfFactory.supports["COLLECTIONS"]).to.be.true + console.log(store.statements) expect(store.statements).to.have.length(1); - const statement = store.statements[0] - expect(statement.subject.value).to.equal('https://www.example.org/#me') - expect(statement.object.value).to.equal('https://www.example.org/abc/xyz') + + const collection = store.statements[0] + expect(collection.subject.value).to.equal('https://www.example.org/#me') + expect(collection.predicate.value).to.equal('https://example.org/ns#listProp') + expect(collection.object.termType).to.equal('Collection') + expect(collection.object.elements.length).to.equal(3) + + expect(collection.object.elements[0].termType).to.equal('Literal') + expect(collection.object.elements[0].datatype.value).to.equal(defaultXSD.string.value) + expect(collection.object.elements[0].value).to.equal(`list item 0`) + + expect(collection.object.elements[1].termType).to.equal('Literal') + expect(collection.object.elements[1].datatype.value).to.equal(defaultXSD.integer.value) + expect(collection.object.elements[1].value).to.equal(`1`) + + expect(collection.object.elements[2].termType).to.equal('NamedNode') + expect(collection.object.elements[2].value).to.equal(`http://example.com/2`) + }) }) })