import ClassOrder from './class-order' import Collection from './collection' import CanonicalDataFactory from './factories/canonical-data-factory' import log from './log' import RDFlibNamedNode from './named-node' import Namespace from './namespace' import Node from './node-internal' import serialize from './serialize' import Statement from './statement' import { Bindings, GraphTermType, } from './types' import { isStatement } from './utils/terms' import Variable from './variable' import { Indexable, TFIDFactoryTypes, } from './factories/factory-types' import { appliedFactoryMethods, arrayToStatements } from './utils' import { RdfJsDataFactory, Quad_Graph, Quad_Object, Quad_Predicate, Quad, Quad_Subject, Term, } from './tf-types' import Fetcher from './fetcher' import BlankNode from './blank-node' import NamedNode from './named-node' export interface FormulaOpts { dataCallback?: (q: Quad) => void rdfArrayRemove?: (arr: Quad[], q: Quad) => void rdfFactory?: RdfJsDataFactory } interface BooleanMap { [uri: string]: boolean; } interface MembersMap { [uri: string]: Quad; } interface UriMap { [uri: string]: string; } /** * A formula, or store of RDF statements */ export default class Formula extends Node { termType: typeof GraphTermType = GraphTermType classOrder = ClassOrder.Graph /** * The accompanying fetcher instance. * * Is set by the fetcher when initialized. */ fetcher?: Fetcher isVar = 0 /** * A namespace for the specified namespace's URI * @param nsuri The URI for the namespace */ ns = Namespace /** The factory used to generate statements and terms */ rdfFactory: any /** * Initializes this formula * @constructor * @param statements - Initial array of statements * @param constraints - initial array of constraints * @param initBindings - initial bindings used in Query * @param optional - optional * @param opts * @param opts.rdfFactory - The rdf factory that should be used by the store */ constructor ( public statements: Array = [], public constraints: ReadonlyArray = [], public initBindings: ReadonlyArray = [], public optional: ReadonlyArray = [], opts: FormulaOpts = {} ) { super('') this.rdfFactory = (opts && opts.rdfFactory) || CanonicalDataFactory // Enable default factory methods on this while preserving factory context. for(const factoryMethod of appliedFactoryMethods) { this[factoryMethod] = (...args) => this.rdfFactory[factoryMethod](...args) } } /** Add a statement from its parts * @param subject - the first part of the statement * @param predicate - the second part of the statement * @param object - the third part of the statement * @param graph - the last part of the statement */ add ( subject: Quad_Subject | Quad | Quad[], predicate?: Quad_Predicate, object?: Term | string, graph?: Quad_Graph ): Statement | null | this | number { if (arguments.length === 1) { (subject as Quad[]).forEach(st => this.add(st.subject, st.predicate, st.object, st.graph)) } return this.statements.push(this.rdfFactory.quad(subject, predicate, object, graph)) } /** Add a statment object * @param {Statement} statement - An existing constructed statement to add */ addStatement (statement: Quad): Statement | null | this | number { return this.add(statement) } /** * Shortcut for adding blankNodes * @param [id] */ bnode (id?: string): BlankNode { return this.rdfFactory.blankNode(id) } /** * Adds all the statements to this formula * @param statements - A collection of statements */ addAll (statements: Quad[]): void { statements.forEach(quad => { this.add(quad.subject, quad.predicate, quad.object, quad.graph) }) } /** Follow link from one node, using one wildcard, looking for one * * For example, any(me, knows, null, profile) - a person I know accoring to my profile . * any(me, knows, null, null) - a person I know accoring to anything in store . * any(null, knows, me, null) - a person who know me accoring to anything in store . * * @param s - A node to search for as subject, or if null, a wildcard * @param p - A node to search for as predicate, or if null, a wildcard * @param o - A node to search for as object, or if null, a wildcard * @param g - A node to search for as graph, or if null, a wildcard * @returns A node which match the wildcard position, or null */ any( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null ): Node | null { const st = this.anyStatementMatching(s, p, o, g) if (st == null) { return null } else if (s == null) { return st.subject } else if (p == null) { return st.predicate } else if (o == null) { return st.object } return null } /** * Gets the value of a node that matches the specified pattern * @param s The subject * @param p The predicate * @param o The object * @param g The graph that contains the statement */ anyValue( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null ): string | void { const y = this.any(s, p, o, g) return y ? y.value : void 0 } /** * Gets the first JavaScript object equivalent to a node based on the specified pattern * @param s The subject * @param p The predicate * @param o The object * @param g The graph that contains the statement */ anyJS( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null ): any { const y = this.any(s, p, o, g) return y ? Node.toJS(y) : void 0 } /** * Gets the first statement that matches the specified pattern */ anyStatementMatching( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null ): Statement | undefined { let x = this.statementsMatching(s, p, o, g, true) if (!x || x.length === 0) { return undefined } return x[0] } /** * Returns a unique index-safe identifier for the given term. * * Falls back to the rdflib hashString implementation if the given factory doesn't support id. */ id (term: TFIDFactoryTypes): Indexable { return this.rdfFactory.id(term) } /** * Search the Store * This is really a teaching method as to do this properly you would use IndexedFormula * * @param s - A node to search for as subject, or if null, a wildcard * @param p - A node to search for as predicate, or if null, a wildcard * @param o - A node to search for as object, or if null, a wildcard * @param g - A node to search for as graph, or if null, a wildcard * @param justOne - flag - stop when found one rather than get all of them? * @returns {Array} - An array of nodes which match the wildcard position */ statementsMatching( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null, justOne?: boolean ): Statement[] { const sts = this.statements.filter(st => (!s || s.equals(st.subject)) && (!p || p.equals(st.predicate)) && (!o || o.equals(st.object)) && (!g || g.equals(st.graph)) ) if (justOne) { return sts.length === 0 ? [] : [sts[0]] } return sts } /** * Finds the types in the list which have no *stored* subtypes * These are a set of classes which provide by themselves complete * information -- the other classes are redundant for those who * know the class DAG. * @param types A map of the types */ bottomTypeURIs (types) { let bots let bottom let elt let i let len let ref let subs let v bots = [] for (let k in types) { if (!types.hasOwnProperty(k)) continue v = types[k] subs = this.each( void 0, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#subClassOf'), this.rdfFactory.namedNode(k) ) bottom = true i = 0 for (len = subs.length; i < len; i++) { elt = subs[i] ref = elt.uri if (ref in types) { // the subclass is one we know bottom = false break } } if (bottom) { bots[k] = v } } return bots } /** Creates a new collection */ collection (): Collection { return new Collection() } /** Follow links from one node, using one wildcard. * * For example, each(me, knows, null, profile) - people I know accoring to my profile . * each(me, knows, null, null) - people I know accoring to anything in store . * each(null, knows, me, null) - people who know me accoring to anything in store . * * @param s - A node to search for as subject, or if null, a wildcard * @param p - A node to search for as predicate, or if null, a wildcard * @param o - A node to search for as object, or if null, a wildcard * @param g - A node to search for as graph, or if null, a wildcard * @returns {Array} - An array of nodes which match the wildcard position */ each( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null ): Node[] { const results: Node[] = [] let sts = this.statementsMatching(s, p, o, g, false) if (s == null) { for (let i = 0, len = sts.length; i < len; i++) { results.push(sts[i].subject) } } else if (p == null) { for (let l = 0, len1 = sts.length; l < len1; l++) { results.push(sts[l].predicate) } } else if (o == null) { for (let m = 0, len2 = sts.length; m < len2; m++) { results.push(sts[m].object) } } else if (g == null) { for (let q = 0, len3 = sts.length; q < len3; q++) { results.push(new NamedNode(sts[q].graph.value)) } } return results } /** * Test whether this formula is equals to {other} * @param other - The other formula */ equals(other: Formula): boolean { if (!other) { return false } return this.hashString() === other.hashString() } /** * For thisClass or any subclass, anything which has it is its type * or is the object of something which has the type as its range, or subject * of something which has the type as its domain * We don't bother doing subproperty (yet?)as it doesn't seeem to be used * much. * Get all the Classes of which we can RDFS-infer the subject is a member * @return a hash of URIs */ findMembersNT (thisClass) { let len2: number let len4: number let m: number let members: MembersMap let pred: Quad_Predicate let ref let ref1: Quad[] let ref2: Term[] let ref3: Quad[] let ref4: Term[] let ref5: Quad[] let seeds let st let u: number seeds = {} seeds[thisClass.toNT()] = true members = {} ref = this.transitiveClosure(seeds, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#subClassOf'), true) for (let t in ref) { if (!ref.hasOwnProperty(t)) continue ref1 = this.statementsMatching(void 0, this.rdfFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), this.fromNT(t)) for (let i = 0, len = ref1.length; i < len; i++) { st = ref1[i] members[st.subject.toNT()] = st } ref2 = this.each(void 0, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#domain'), this.fromNT(t)) for (let l = 0, len1 = ref2.length; l < len1; l++) { pred = ref2[l] as Quad_Predicate ref3 = this.statementsMatching(void 0, pred) for (m = 0, len2 = ref3.length; m < len2; m++) { st = ref3[m] members[st.subject.toNT()] = st } } ref4 = this.each(void 0, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#range'), this.fromNT(t)) for (let q = 0, len3 = ref4.length; q < len3; q++) { pred = ref4[q] as Quad_Predicate ref5 = this.statementsMatching(void 0, pred) for (u = 0, len4 = ref5.length; u < len4; u++) { st = ref5[u] members[st.object.toNT()] = st } } } return members } /** * For thisClass or any subclass, anything which has it is its type * or is the object of something which has the type as its range, or subject * of something which has the type as its domain * We don't bother doing subproperty (yet?)as it doesn't seeem to be used * much. * Get all the Classes of which we can RDFS-infer the subject is a member * @param subject - A named node */ findMemberURIs(subject: Node): UriMap { return this.NTtoURI(this.findMembersNT(subject)) } /** * Get all the Classes of which we can RDFS-infer the subject is a superclass * Returns a hash table where key is NT of type and value is statement why we * think so. * Does NOT return terms, returns URI strings. * We use NT representations in this version because they handle blank nodes. */ findSubClassesNT(subject: Node): { [uri: string]: boolean } { let types = {} types[subject.toNT()] = true return this.transitiveClosure( types, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#subClassOf'), true ) } /** * Get all the Classes of which we can RDFS-infer the subject is a subclass * @param {RDFlibNamedNode} subject - The thing whose classes are to be found * @returns a hash table where key is NT of type and value is statement why we * think so. * Does NOT return terms, returns URI strings. * We use NT representations in this version because they handle blank nodes. */ findSuperClassesNT(subject: Node): { [uri: string]: boolean } { let types = {} types[subject.toNT()] = true return this.transitiveClosure(types, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#subClassOf'), false) } /** * Get all the Classes of which we can RDFS-infer the subject is a member * todo: This will loop is there is a class subclass loop (Sublass loops are * not illegal) * @param {RDFlibNamedNode} subject - The thing whose classes are to be found * @returns a hash table where key is NT of type and value is statement why we think so. * Does NOT return terms, returns URI strings. * We use NT representations in this version because they handle blank nodes. */ findTypesNT (subject) { let domain let range let rdftype let ref let ref1 let ref2 let ref3 let st let types rdftype = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' types = [] ref = this.statementsMatching(subject, void 0, void 0) for (let i = 0, len = ref.length; i < len; i++) { st = ref[i] if (st.predicate.uri === rdftype) { types[st.object.toNT()] = st } else { ref1 = this.each( st.predicate, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#domain') ) for (let l = 0, len1 = ref1.length; l < len1; l++) { range = ref1[l] types[range.toNT()] = st } } } ref2 = this.statementsMatching(void 0, void 0, subject) for (let m = 0, len2 = ref2.length; m < len2; m++) { st = ref2[m] ref3 = this.each( st.predicate, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#range') ) for (let q = 0, len3 = ref3.length; q < len3; q++) { domain = ref3[q] types[domain.toNT()] = st } } return this.transitiveClosure( types, this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#subClassOf'), false ) } /** * Get all the Classes of which we can RDFS-infer the subject is a member * todo: This will loop is there is a class subclass loop (Sublass loops are * not illegal) * Returns a hash table where key is NT of type and value is statement why we * think so. * Does NOT return terms, returns URI strings. * We use NT representations in this version because they handle blank nodes. * @param subject - A subject node */ findTypeURIs(subject: Quad_Subject): UriMap { return this.NTtoURI(this.findTypesNT(subject)) } /** Trace statements which connect directly, or through bnodes * * @param subject - The node to start looking for statments * @param doc - The document to be searched, or null to search all documents * @returns an array of statements, duplicate statements are suppresssed. */ connectedStatements( subject: Quad_Subject, doc: Quad_Graph, excludePredicateURIs?: ReadonlyArray ): Statement[] { excludePredicateURIs = excludePredicateURIs || [] let todo = [subject] let done: { [k: string]: boolean } = {} let doneArcs: { [k: string]: boolean } = {} let result: Statement[] = [] let self = this let follow = function (x) { let queue = function (x) { if (x.termType === 'BlankNode' && !done[x.value]) { done[x.value] = true todo.push(x) } } let sts = self.statementsMatching(null, null, x, doc) .concat(self.statementsMatching(x, null, null, doc)) sts = sts.filter(function (st): boolean { if (excludePredicateURIs![st.predicate.value]) return false let hash = (st as Statement).toNT() if (doneArcs[hash]) return false doneArcs[hash] = true return true } ) sts.forEach(function (st) { queue(st.subject) queue(st.object) }) result = result.concat(sts) } while (todo.length) { follow(todo.shift()) } return result } /** * Creates a new empty formula * * @param _features - Not applicable, but necessary for typing to pass */ formula(_features?: ReadonlyArray): Formula { return new Formula() } /** * Transforms an NTriples string format into a Node. * The blank node bit should not be used on program-external values; designed * for internal work such as storing a blank node id in an HTML attribute. * This will only parse the strings generated by the various toNT() methods. */ fromNT (str) { let dt, k, lang switch (str[0]) { case '<': return this.sym(str.slice(1, -1)) case '"': lang = void 0 dt = void 0 k = str.lastIndexOf('"') if (k < str.length - 1) { if (str[k + 1] === '@') { lang = str.slice(k + 2) } else if (str.slice(k + 1, k + 3) === '^^') { dt = this.fromNT(str.slice(k + 3)) } else { throw new Error("Can't convert string from NT: " + str) } } str = str.slice(1, k) str = str.replace(/\\"/g, '"') str = str.replace(/\\n/g, '\n') str = str.replace(/\\\\/g, '\\') return this.rdfFactory.literal(str, lang || dt) case '_': return this.rdfFactory.blankNode(str.slice(2)) case '?': return new Variable(str.slice(1)) } throw new Error("Can't convert from NT: " + str) } /** Returns true if this formula holds the specified statement(s) */ holds (s: any | any[], p?, o?, g?): boolean { let i if (arguments.length === 1) { if (!s) { return true } if (s instanceof Array) { for (i = 0; i < s.length; i++) { if (!this.holds(s[i])) { return false } } return true } else if (isStatement(s)) { return this.holds(s.subject, s.predicate, s.object, s.graph) } else if (s.statements) { return this.holds(s.statements) } } let st = this.anyStatementMatching(s, p, o, g) return st != null } /** * Returns true if this formula holds the specified {statement} */ holdsStatement (statement) { return this.holds( statement.subject, statement.predicate, statement.object, statement.graph, ) } /** * Used by the n3parser to generate list elements * @param values - The values of the collection * @param context - The store * @return {BlankNode|Collection} - The term for the statement */ list (values, context) { if (context.rdfFactory.supports["COLLECTIONS"]) { const collection = context.rdfFactory.collection() values.forEach(function (val) { collection.append(val) }) return collection } else { const node = context.rdfFactory.blankNode() const statements = arrayToStatements(context.rdfFactory, node, values) context.addAll(statements) return node } } /** * Transform a collection of NTriple URIs into their URI strings * @param t - Some iterable collection of NTriple URI strings * @return A collection of the URIs as strings * todo: explain why it is important to go through NT */ NTtoURI (t) { let k, v let uris = {} for (k in t) { if (!t.hasOwnProperty(k)) continue v = t[k] if (k[0] === '<') { uris[k.slice(1, -1)] = v } } return uris } /** * Serializes this formula * @param base - The base string * @param contentType - The content type of the syntax to use * @param provenance - The provenance URI * @param options - options to pass to the serializer, as defined in serialize method */ serialize (base, contentType, provenance, options?) { // delegate the graph serialization to the implementation in ./serialize return serialize(provenance, this, base, contentType, undefined, options); } /** * Creates a new formula with the substituting bindings applied * @param bindings - The bindings to substitute */ substitute(bindings: Bindings): T { let statementsCopy = this.statements.map(function (ea) { return (ea as Statement).substitute(bindings) }) // console.log('Formula subs statmnts:' + statementsCopy) const y = new Formula() y.addAll(statementsCopy as Quad[]) // console.log('indexed-form subs formula:' + y) return y as unknown as T } sym (uri: string, name?): NamedNode { if (name) { throw new Error('This feature (kb.sym with 2 args) is removed. Do not assume prefix mappings.') } return this.rdfFactory.namedNode(uri) } /** * Gets the node matching the specified pattern. Throws when no match could be made. * @param s - The subject * @param p - The predicate * @param o - The object * @param g - The graph that contains the statement */ the ( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null ): Node | null | undefined { let x = this.any(s, p, o, g) if (x == null) { log.error('No value found for the() {' + s + ' ' + p + ' ' + o + '}.') } return x } /** * RDFS Inference * These are hand-written implementations of a backward-chaining reasoner * over the RDFS axioms. * @param seeds - A hash of NTs of classes to start with * @param predicate - The property to trace though * @param inverse - Trace inverse direction */ transitiveClosure( seeds: BooleanMap, predicate: Quad_Predicate, inverse?: boolean ): { [uri: string]: boolean; } { let elt, i, len, s, sups, t let agenda = {} Object.assign(agenda, seeds) // make a copy let done = {} // classes we have looked up while (true) { t = (function () { for (let p in agenda) { if (!agenda.hasOwnProperty(p)) continue return p } })() if (t == null) { return done } sups = inverse ? this.each(void 0, predicate, this.fromNT(t)) : this.each(this.fromNT(t) as Quad_Predicate, predicate) for (i = 0, len = sups.length; i < len; i++) { elt = sups[i] s = elt.toNT() if (s in done) { continue } if (s in agenda) { continue } agenda[s] = agenda[t] } done[t] = agenda[t] delete agenda[t] } } /** * Finds the types in the list which have no *stored* supertypes * We exclude the universal class, owl:Things and rdf:Resource, as it is * information-free. * @param types - The types */ topTypeURIs(types: { [id: string]: string | RDFlibNamedNode; }): { [id: string]: string | RDFlibNamedNode; } { let i let j let k let len let n let ref let tops let v tops = [] for (k in types) { if (!types.hasOwnProperty(k)) continue v = types[k] n = 0 ref = this.each( this.rdfFactory.namedNode(k), this.rdfFactory.namedNode('http://www.w3.org/2000/01/rdf-schema#subClassOf') ) for (i = 0, len = ref.length; i < len; i++) { j = ref[i] if (j.uri !== 'http://www.w3.org/2000/01/rdf-schema#Resource') { n++ break } } if (!n) { tops[k] = v } } if (tops['http://www.w3.org/2000/01/rdf-schema#Resource']) { delete tops['http://www.w3.org/2000/01/rdf-schema#Resource'] } if (tops['http://www.w3.org/2002/07/owl#Thing']) { delete tops['http://www.w3.org/2002/07/owl#Thing'] } return tops } /** * Serializes this formula to a string */ toString(): string { return '{' + this.statements.join('\n') + '}' } /** * Gets a new variable * @param name - The variable's name */ public variable(name: string): Variable { return new Variable(name) } /** * Gets the number of statements in this formula that matches the specified pattern * @param s - The subject * @param p - The predicate * @param o - The object * @param g - The graph that contains the statement */ whether( s?: Quad_Subject | null, p?: Quad_Predicate | null, o?: Quad_Object | null, g?: Quad_Graph | null ): number { return this.statementsMatching(s, p, o, g, false).length } }