// initialize error handler
const Boom = require('@hapi/boom')
const ReasoningService = require('../services/reasoning.js')
const BaseModel = require('./baseModel.js')
// RDF stuff
const N3 = require('n3')
const { DataFactory } = N3
const { namedNode, literal, defaultGraph, quad } = DataFactory
// for creating path names to files
const path = require('path')
const fs = require('fs')
const assert = require('assert').strict
/**
* Class to operate on citizen data
* @extends BaseModel
*/
class CitizenModel extends BaseModel {
constructor (req, solidService) {
super(req)
this.solidService = solidService
if (this.solidService !== null) {
this.solidService.uploadProfileToPersonalStorage(req.body)
}
this.extendedFolder = '../reasoning/profile'
this.fileName = 'profileInfo.n3'
this.fullFilePath = path.join(this.dataFolder, this.extendedFolder, this.fileName)
}
/**
* Returns a promise resolving to all the information available about a specific step
* Invokes the reasoner to achieve this
* @async
* @param {string} stepName - name of the step (without "step:" prefix)
* @returns {Promise<string>} - Promise resolving to N3-string containing all the information
*/
async getInfoByStep (stepName) {
if (stepName.includes(' ')) { return Promise.reject(Boom.badRequest('Step cannot contain any whitespaces.'), null) }
let myQuad = null
// use full URI or step label depending on what was supplied
if (stepName.includes('http://')) {
// create quad needed for reasoning query
// eg: step:showWasteCollection ex:displayInput true.
myQuad = quad(
namedNode(stepName),
namedNode('http://example.org/ns/example#' + 'displayInput'),
literal(true),
defaultGraph()
)
} else {
myQuad = quad(
namedNode(this.prefixes.step + stepName),
namedNode('http://example.org/ns/example#' + 'displayInput'),
literal(true),
defaultGraph()
)
}
const quads = [myQuad]
const prefixes = { step: this.prefixes.step, ' ': this.prefixes[' '] }
// convert the quad to a file to be used by reasoner
const tempFile = await this.fileService.quadsToTempFile(quads, prefixes)
const inference = {
query: 'reasoning/show/query_used.n3',
data: [
'reasoning/profile/knowledge.n3',
'reasoning/profile/personalInfo.n3',
'reasoning/show/getInfo.n3',
'reasoning/interim/show/query-patterns.n3 ',
'reasoning/interim/steps/component-level-steps.n3',
'reasoning/interim/steps/container-level-steps.n3',
'reasoning/interim/steps/journey-level-steps.n3',
'reasoning/help-functions/built-ins.n3',
tempFile
]
}
if (this.solidService !== null) {
this.solidService.injectUserStorageLocation(inference)
} else {
inference.data.push(this.fullFilePath)
}
// Here comes the all the DAG invocation
const result = await ReasoningService.eyePromise(inference)
fs.unlink(tempFile, (err) => {
console.log(`Unlinking ${tempFile}`)
if (err) throw err
})
if (this.solidService !== null) {
this.solidService.removeUserData()
}
return result
}
/**
* Add new information on a user
* @async
* @param {string} n3string - N3-string to be added to the users profile file
* @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
*/
async addInfoByN3String (n3string) {
// define the file we want to append to
// var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;
// let file = path.join(this.dataFolder, this.extendedFolder,this.fileName);
// create the file if it doesn't exist
// const handle = await this.fileService.newFile(this.fullFilePath);
return this.fileService.appendN3stringToFile(this.fullFilePath, n3string)
// .then(() => handle.close());
}
/**
* Edit info of a user
* @param {Quad[]} quads - old quads that should change
* @param {Array} values - new values to apply to old quad
* @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
*/
async editInfoByQuads (quads, values) {
// make sure array format was supplied
if (!Array.isArray(quads)) {
console.log("Passing scalar quad to 'citizenModel::editInfoByQuads'")
quads = [quads]
}
if (!Array.isArray(values)) {
console.log("Passing scalar value to 'citizenModel::editInfoByQuads'")
values = [values]
}
// file that holds all user info
// var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;
// get existing quads and prefixes from file
const fileQuads = await this.fileService.fileToQuads(this.fullFilePath)
const prefixes = await this.fileService.prefixesFromFile(this.fullFilePath)
// store the user's quads, so we can easily manipulate them
const store = new N3.Store(fileQuads)
for (const i in quads) {
// remove the old quad from the store
store.removeQuad(quads[i])
// change the quad to match the new value
quads[i].object = literal(values[i])
}
// retrieve the quads from the store after removal of old quads
let storeQuads = store.getQuads()
// add new quads to the storedQuads
storeQuads = storeQuads.concat(quads)
// write changes to file
return this.fileService.writeQuadsToFile(this.fullFilePath, storeQuads, prefixes)
}
/**
* Edit info of a user
* Replaces old quad with new quad with new value
* and persists this to disk
* @async
* @param {Quad} quad - the old quad we want to edit
* @param {} value - new value for the object field of the quad
* @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
*/
async editInfoByQuad (quad, value) {
return this.editInfoByQuads([quad], value)
}
/**
* Retrieves all stored quads that match the supplied patterns
* @async
* @param {string[]} inputPattern - N3 pattern used to match stored quads
* @return {Array} - An array of Quads for every input pattern (this is an array of arrays) or an empty array if no patterns were supplied
*/
async quadsPerInputPattern (patterns) {
if (patterns === null || patterns.length === 0) {
return []
}
try {
const fileQuads = await this.fileService.fileToQuads(this.fullFilePath)
// store the user's stored quads
const store = new N3.Store(fileQuads)
const foundPerPattern = []
for (const inputPattern of patterns) {
if (inputPattern === null) {
continue
}
const inputQuads = this.fileService.stringToQuads(inputPattern.prefixes + '\r\n' + inputPattern.triples + '.')
let foundQuads = []
for (const quad of inputQuads) {
foundQuads = foundQuads.concat(store.getQuads(quad.subject, quad.predicate, null))
}
foundPerPattern.push(foundQuads)
}
return foundPerPattern
} catch (err) {
console.error(err)
return []
}
}
/**
* Retrieves all stored quads that match the pattern
* @param {String} inputPattern - N3 pattern used to match stored quads
* @return {Array} - The found quads or an empty array if no patterns were found
*/
getAllQuadsByInputPattern (inputPattern) {
const found = this.quadsPerInputPattern([inputPattern])
// Were only providing a single pattern so the list of quadlists should have at most 1 element
// If no elements are found just return an empty list because found[0] might not exist
assert.strictEqual(found.length <= 1, true, 'More quadlists returned then input patterns provided')
if (found.length === 1) { return found[0] }
return []
}
/**
* Delete all stored quads matching the input patterns templates provided
* @async
* @param {Array} - Array of patterns to remove
* @returns {void}
* @todo Use removeMatches instead of manually retrieving quads by quadsPerInputPattern and deleting them
*/
async deleteQuadsByInputPatterns (inputPatterns) {
if (inputPatterns === null) {
return 0
}
// make sure array format was supplied
if (!Array.isArray(inputPatterns)) {
inputPatterns = [inputPatterns]
console.warn('Passing scalar value to citizenModel::deleteQuadsByInputPatterns')
}
// get all of the quads we stored for the user, get prefixes too to keep files clean
const fileQuads = await this.fileService.fileToQuads(this.fullFilePath)
const prefixes = await this.fileService.prefixesFromFile(this.fullFilePath)
// initialize an N3 store with the user's stored quads
const store = new N3.Store(fileQuads)
const quads = this.quadsPerInputPattern(inputPatterns)
const flatQuads = quads.flat()
store.removeQuads(flatQuads)
// get the remaining quads from the store
const remainingQuads = store.getQuads()
/* TODO: Check if this is equivalent to the above
// initialize an N3 store with the user's stored quads
const store = new N3.Store(fileQuads);
for(const pattern of inputPatterns){
store.removeMatches(pattern);
}
const remainingQuads = store.getQuads();
*/
// write changes to file
try {
await this.fileService.writeQuadsToFile(this.fullFilePath, remainingQuads, prefixes)
} catch (e) {
console.error('deleteQuadsByInputPatterns: error writing quads back to file')
}
}
/*
* Delete all info on user,
* ! currently clears the entire storage file
*/
/**
* Delete all info on user,
* ! currently clears the entire storage file
* @returns {Promise<void>} - Returns promise resolving to a value that can be discarded
*/
async deleteAllInfo () {
// define the file we want to append to
// var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;
return this.fileService.deleteContent(this.fullFilePath)
}
}
module.exports = CitizenModel
/*
* Delete a filled in input pattern by using the input pattern template
*/
/* async deleteQuadsByInputPattern(inputPattern)
{
return deleteQuadsByInputPatterns([inputPattern]);
// get the quad endpoint quad for this pattern if it exists
const quad = await this.getQuadByInputPattern(inputPattern);
if (quad == null){
console.log('No data to delete');
return new Promise((resolve) => {
resolve()
}); //next(Boom.badRequest('No existing info to delete.'), null)
}
// file that holds all user info
//var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;
// get all of the quads we stored for the user, get prefixes too to keep files clean
const fileQuads = await this.fileService.fileToQuads(this.fullFilePath);
const prefixes = await this.fileService.prefixesFromFile(this.fullFilePath);
// initialize an N3 store with stored quads
const store = new N3.Store(fileQuads);
// use this quad to work way back to find the other stored quads for this pattern
//TODO: why overwrite?
let quads = store.getQuads(null, null, quad.subject);
quads = store.getQuads(quad.subject, null, null);
// remove the quads we found from the store
store.removeQuads(quads);
// get the remaining quads from the store
const storeQuads = store.getQuads();
// write changes to file
return this.fileService.writeQuadsToFile(this.fullFilePath, storeQuads, prefixes);
}
*/
/*
* Find if a filled in input pattern already exists for each of the patterns in inputPatterns,
* send back the endpoint quads (which hold the info) for each of the patterns.
*/
/*
async getQuadsByInputPatterns(inputPatterns) {
if (!Array.isArray(inputPatterns))
inputPatterns = [inputPatterns];
let quadPromises = [];
for(let pattern of inputPatterns){
quadPromises.push(this.getQuadByInputPattern(pattern));
}
//await Promise.all(inputPatterns.map(pattern => this.getQuadByInputPattern(pattern)));
let quads = await Promise.all(quadPromises);
return quads;
}
*/
/*
* Find if a filled in input pattern already exists,
* send back the stored endpoint quad (which holds the info) for this pattern.
* Returns null if there doesn't exist a stored quad
* or the inputPattern is null (i.e. it doesn't specify a variable to store)
*/
/*
async getQuadByInputPattern(inputPattern) {
if (inputPattern === null) {
return null;
}
// file that holds all user info
//var file = this.dataFolder + '/' + this.extendedFolder + '/' + this.fileName;
// get all of the quads
const fileQuads = await this.fileService.fileToQuads(this.fullFilePath);
const inputQuads = this.fileService.stringToQuads(inputPattern.prefixes + '\r\n' + inputPattern.triples + '.');
let replaceQuad = this.fileService.stringToQuads(inputPattern.prefixes + '\r\n' + inputPattern.replaceVar)[0];
// store the user's stored quads
const store = new N3.Store(fileQuads);
// store the input pattern quads
const inputPatternStore = new N3.Store(inputQuads);
// use the input patterns store to search the quad containing the info on the step
replaceQuad = inputPatternStore.getQuads(null, null, replaceQuad.object)[0];
const keepPredicate = replaceQuad.predicate;
inputPatternStore.removeQuad(replaceQuad);
replaceQuad = inputPatternStore.getQuads(replaceQuad.subject, null, null)[0];
let storedQuad = null;
if (replaceQuad == null) {
replaceQuad = inputPatternStore.getQuads(null, null, null)[0];
if (replaceQuad != null) {
storedQuad = store.getQuads(null, replaceQuad.predicate, null)[0];
}
if (storedQuad != null) {
storedQuad = store.getQuads(storedQuad.object, keepPredicate, null)[0];
}
if (storedQuad == null) {
storedQuad = store.getQuads(null, keepPredicate, null)[0];
}
} else {
if (replaceQuad != null) {
storedQuad = store.getQuads(null, null, replaceQuad.object)[0];
}
if (storedQuad != null) {
storedQuad = store.getQuads(storedQuad.subject, keepPredicate, null)[0];
}
}
// will return null if user doesn't have anything stored for this pattern
return storedQuad;
}
*/