@rmlio/matey
Version:
Web-based editor for YARRRML rules.
460 lines (381 loc) • 14.5 kB
JavaScript
// include all necessary dependencies
require('popper.js');
require('bootstrap');
const ace = require("brace");
require('brace/theme/monokai');
require('brace/mode/yaml');
require('brace/mode/json');
require('brace/mode/text');
require('brace/mode/xml');
const fs = require("fs");
const yarrrml = require("@rmlio/yarrrml-parser/lib/rml-generator");
const N3 = require("n3");
const Persister = require("./util/persister");
// import CSS style sheet
require('../assets/css/style.css');
// utilities
const {prettifyRDF, loadRemote} = require("./util/util");
const quadsSorter = require("./sorters/quadssorter");
// front
const Front = require('./front');
// EditorManager
const EditorManager = require('./editor-manager');
/**
* Class for adding/manipulating Matey content in web page
*/
class Matey {
constructor() {
// read yaml prefixes from prefixes.json
this.yamlPrefixes = JSON.parse(fs.readFileSync(__dirname + '/resources/prefixes.json', 'utf8'));
// read Matey examples from examples.json
this.mateyExamples = JSON.parse(fs.readFileSync(__dirname + '/resources/examples.json', 'utf8'));
// create persister
this.persister = new Persister();
// create logger
this.logger = require('./logger');
// YARRML to RML convertor
this.y2r = new yarrrml();
// matey EditorManager
this.editorManager = new EditorManager(this);
// matey front
this.front = new Front(this);
// An identifier for stateful functions performed during RML mappings, e.g., LDES generation.
// When invoked from a browser it is the timestamp of refreshing the page, so as long as
// the user doesn't reload the page, this id remains the same and the state is not reset.
this.functionStateId = new Date().getTime().toString();
}
/**
* Initializes all of the Matey content in <div> element with given id
* @param {String} id - id of <div> element into which HTML code with Matey content should be inserted
* @param {Object} config - object with user configuration
*/
init(id, config) {
this.id = id;
// check if rmlMapperUrl is set in config
if (config.rmlMapperUrl) {
this.rmlMapperUrl = config.rmlMapperUrl;
} else {
throw new Error("No RMLMapper URL specified. Make sure the 'rmlMapperUrl' property is set in the configuration object.");
}
// warn logger that page has been visited
this.logger.warn('page_visit');
// initialize the front-end
this.front.init()
// initialize the EditorManager
this.editorManager.init()
// load examples into YARRRML and Data editors
this.front.loadExamples('examples-matey', this.mateyExamples);
const stored = this.persister.get('latestExample');
if (stored) {
this.front.doAlert('We found a previous edited state in our LocalStorage you used to successfully generate RDF! I hope you don\'t mind we loaded that one for you ;).', 'info', 10000);
this.loadExample(stored);
} else {
this.loadExample(this.mateyExamples[0]);
}
}
/**
* This function "clears" the state by assigning a new timestamp to `functionStateId`.
* This way RMLMapper Web API uses a new (clean) state when calling `toRML()` the next time.
*/
clearAll() {
this.editorManager.clearAll();
this.functionStateId = new Date().getTime().toString();
this.front.doAlert('Everything cleared!', 'success');
}
/**
* Fetch a remote data source, and use it to create a new data editor. Displays alert if fetching fails.
* @param {String} url - url of the remote data source
* @param {String} dataPath - path that data source will have once it's loaded in
* @returns {Promise<String>} promise that holds data source if fetch successful
*/
loadRemoteDataSource(url, dataPath) {
return loadRemote(url)
.then(dataValue => {
this.editorManager.createAndOpenDataEditor(dataPath, dataValue);
})
.catch(e => {
this.logger.error('data_loading_error', e);
this.front.doAlert('Could not load remote data source.', 'danger', 5000);
})
}
/**
* Fetch YARRRML rules from remote source and set YARRRML editor's value to them. Displays alert if fetching fails.
* @param {String} url - url of remote rules
* @returns {Promise<String>} promise that holds data source if fetch successful
*/
loadRemoteYarrrml(url) {
return loadRemote(url)
.then(rules => {
this.editorManager.setInput(rules);
})
.catch(e => {
this.logger.error('yarrrml_loading_error', e);
this.front.doAlert('Could not load remote YARRRML rules.', 'danger', 5000);
})
}
/**
* Converts inserted YARRRML rules to RML rules and inserts it into RML editor.
* @returns {Promise<void>} promise that resolves if RML was successfully generated, and rejects otherwise
*/
toRML() {
return new Promise((resolve, reject) => {
try {
const yaml = this.editorManager.getInput();
const y2r = new yarrrml();
const quads = this.generateRMLQuads(y2r);
if (!quads) {
resolve();
}
quads.sort(quadsSorter);
const writer = new N3.Writer({
prefixes: {
rr: 'http://www.w3.org/ns/r2rml#',
rml: 'http://semweb.mmlab.be/ns/rml#',
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
ql: 'http://semweb.mmlab.be/ns/ql#',
map: y2r.baseIRI,
...this.getYamlPrefixes()
}
});
writer.addQuads(quads);
writer.end(async (error, result) => {
// try to prettify result
try {
result = await prettifyRDF(result);
} catch (e) {
this.logger.error('prettify_failed', e)
}
// set result as RML output
const outputEditor = this.editorManager.createOutputEditor({path: 'mapping.rml.ttl', type: 'text', value: result}, 'RML mapping', 0);
this.logger.warn('rml_generated', {yarrrml: yaml, rml: result});
this.front.doAlert('RML mapping file updated!', 'success');
// resolve promise
resolve();
});
} catch (err) {
this.logger.error('rml_generation_error', err);
this.front.doAlert('Could not generate RML rules.', 'danger', 3000);
}
});
}
/**
* Generates RDF quads based on input data and YARRRML rules and inserts them into 'Turtle/TriG' editor.
* @returns {Promise<void>} promise that resolves if LD was successfully generated, and rejects otherwise
*/
runMappingRemote() {
return new Promise((resolve, reject) => {
const yaml = this.editorManager.getInput();
this.toRML();
try {
const quads = this.generateRMLQuads();
if (!quads) {
resolve();
}
let output = [];
const writer = new N3.Writer();
writer.addQuads(quads);
writer.end(async (err, rmlDoc) => {
if (err) {
this.logger.error("Something went wrong when converting with N3 writer.", err);
}
output = this.editorManager.getOutput();
const sources = this.editorManager.getSources();
// Reset outputs
this.editorManager.destroyOutputEditors();
// Execute mapping
const response = await fetch(this.rmlMapperUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
rml: rmlDoc,
functionStateId: this.functionStateId,
sources
})
})
// retrieve JSON from response
const data = await response.json();
console.log(data);
try {
// One output (no targets)
if (typeof data.output === 'string' || data.output instanceof String) {
// parse the generated RDF and convert it to text
const parser = new N3.Parser();
const prefixes = this.getYamlPrefixes();
const outWriter = new N3.Writer({format: 'turtle', prefixes});
parser.parse(data.output, (err, quad) => {
if (quad) {
outWriter.addQuad(quad);
} else {
outWriter.end(async (err, outTtl) => {
// try to prettify RDF
try {
outTtl = await prettifyRDF(outTtl);
} catch (e) {
this.logger.error('prettify_failed', e);
}
// set result as Linked Data output
const outputEditor = this.editorManager.createOutputEditor({path: 'output', type: 'text', value: outTtl}, 'RDF output', 0);
outputEditor.dropdownA.click();
this.front.setOutputButtonDivText('1 RDF output and RML rules');
this.logger.warn('ttl_generated', {output, ttl: data, yarrrml: yaml});
this.front.doAlert('Output updated!', 'success');
// persist data for later use
const persistData = [];
output.forEach(out => {
persistData.push({
path: out.path,
type: out.type,
value: out.data
})
});
this.persister.set('latestExample', {
label: 'latest',
icon: 'user',
yarrrml: yaml,
data: persistData
});
// Resolve promise
resolve();
});
}
});
// Multiple outputs (targets)
} else {
const persistData = [];
console.log(data);
for (const [file, content] of Object.entries(data.output)) {
output.forEach(out => {
persistData.push({
path: out.path,
type: out.type,
value: out.data
})
});
this.persister.set('latestExample', {
label: 'latest',
icon: 'user',
yarrrml: yaml,
data: persistData
});
const outputEditor = this.editorManager.createOutputEditor({path: file, type: 'text', value: content}, 'RDF output');
// Do not focus on targets with no results
if (content !== '') {
outputEditor.dropdownA.click();
}
}
this.front.setOutputButtonDivText(`${Object.keys(data.output).length} RDF outputs and RML rules`);
this.front.doAlert('Output updated!', 'success');
// Resolve promise
resolve();
}
} catch (err) {
this.logger.error('yarrrml_invalid', {yarrrml: yaml});
this.logger.error(err);
this.front.doAlert('Couldn\'t run the YARRRML, check the source.', 'danger');
}
});
} catch (err) {
this.logger.error('ld_generation_error', err);
this.front.doAlert('Could not generate Linked Data.', 'danger', 3000);
}
});
};
/**
* @returns {Object} containing prefixes for YAML rules mapped on their full corresponding IRIs.
*/
getYamlPrefixes() {
const yaml = this.editorManager.getInput();
let prefixes = {};
prefixes.rdf = this.yamlPrefixes.rdf;
Object.keys(this.yamlPrefixes).forEach(pre => {
if (yaml.indexOf(`${pre}:`) >= 0) {
prefixes[pre] = this.yamlPrefixes[pre];
}
});
try {
const json = YAML.parse(yaml);
if (json.prefixes) {
prefixes = Object.assign({}, prefixes, json.prefixes);
}
} catch (e) {
// nothing
}
return prefixes;
}
/**
* Converts the rules from the YARRRML editor into RML rules, and returns the generated quads.
* @param {yarrrml} y2r - object that is used to convert YARRRML into RML
* @returns {Array} array containing generated RDF quads
* @throws {ParseException} exception that gets thrown when input YARRRML is invalid
*/
generateRMLQuads(y2r = null) {
const yaml = this.editorManager.getInput();
if (!y2r) {
y2r = new yarrrml();
}
let quads;
try {
quads = y2r.convert(yaml);
} catch (e) {
this.logger.error('yarrml_invalid', {yarrrml: yaml});
this.front.doAlert('Couldn\'t generate the RML mapping file, check the source.', 'danger');
return null;
}
return quads;
}
/**
* Converts the rules from the YARRRML editor into RML rules, and returns the generated triples.
* @param {yarrrml} y2r - object that is used to convert YARRRML into RML
* @returns {Array} array containing generated RDF quads
* @throws {ParseException} exception that gets thrown when input YARRRML is invalid
*/
generateRMLTriples(y2r = null){
const quads = this.generateRMLQuads(y2r);
const triples = [];
quads.forEach(q => {
triples.push({
subject: q.subject.value,
predicate: q.predicate.value,
object: q.object.termType === 'Literal' ? `"${q.object.value}"` : q.object.value
});
});
return triples;
}
/**
@returns {Array} text inside Turtle/TriG editors
*/
getLD() {
return this.editorManager.getLD();
}
/**
@returns {String} text inside RML editor
*/
getRML() {
return this.editorManager.getRML();
}
/**
@returns {String} text inside YARRRML editor
*/
getYarrrml() {
return this.editorManager.getInput();
}
/**
@returns {String} text inside active data editor
*/
getData() {
return this.editorManager.getData();
}
/**
* Loads in given example into input editors
* @param example - example to be loaded in
* @param reset - Determines the cursor position after example texts are inserted. If true, all text will
* be selected in both input editors. If false, the cursor will move to the top in both input editors.
*/
loadExample(example, reset = false) {
this.editorManager.loadExample(example, reset);
}
}
module.exports = Matey;