UNPKG

gpii-universal

Version:

Cross platform, core components of the GPII personalization infrastructure.

638 lines (586 loc) 30.7 kB
/*! GPII Ontology Server Copyright 2012 OCAD University Copyright 2014 Raising The Floor - International Licensed under the New BSD license. You may not use this file except in compliance with this License. The research leading to these results has received funding from the European Union's Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. You may obtain a copy of the License at https://github.com/GPII/universal/blob/master/LICENSE.txt The ontologyHandler component is responsible for all the functionality related to ontologization, etc., of preference sets. It is not implemented as a kettle server and hence does _not_ expose any URLs - this aspect is taken care of in the preferences server component. */ /* eslint strict: ["error", "function"] */ (function () { "use strict"; var fluid = require("infusion"), gpii = fluid.registerNamespace("gpii"), fs = require("fs"), path = require("path"), JSON5 = require("json5"); fluid.registerNamespace("gpii.ontologyHandler.floydWarshall"); // TODO: Create an incarnation of the ontologyHandler that is not tied to node.js and can be tested // the browser - it will need to be moved to using dataSources for I/O and have an "index" file rather than // calling fs.readdirSync(). fluid.defaults("gpii.ontologyHandler", { gradeNames: ["fluid.component"], invokers: { prefsToOntology: { funcName: "gpii.ontologyHandler.prefsToOntology", args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2" ] }, rawPrefsToOntology: { funcName: "gpii.ontologyHandler.rawPrefsToOntology", args: [ "{that}", "{arguments}.0", "{arguments}.1" ] }, addPrefsToRawPrefs: { funcName: "gpii.ontologyHandler.addPrefsToRawPrefs", args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2" ] } }, listeners: { onCreate: "gpii.ontologyHandler.loadOntologies" }, ontologyTransformationListFolder: "%gpii-universal/testData/ontologies/mappings", ontologyMetadataFolder: "%gpii-universal/testData/ontologies", // directory with ontologies members: { ontologyTransformSpecs: {}, // will hold the actual ontology transformation definitions after read ontologyTransformRoutes: {}, // will hold results of Floyd-Warshall for generating paths between specs ontologyMetadata: {} // will hold the actual ontology metadata after files have been read } }); gpii.ontologyHandler.loadOntologies = function (that) { // Check for re-occurence of GPII-1063 fluid.log("loadOntologies for " + that.id + " path " + fluid.pathForComponent(that)); // Load ontology transformations var transformsFolder = fluid.module.resolvePath(that.options.ontologyTransformationListFolder); var ontologyTransformsList = fs.readdirSync(transformsFolder); // Read the list of available ontologyTransformations // Load the ontologies: gpii.ontologyHandler.loadOntologyTransformSpecs(that, transformsFolder, ontologyTransformsList); // Load the ontology metadata: var metadataFolder = fluid.module.resolvePath(that.options.ontologyMetadataFolder); gpii.ontologyHandler.loadOntologyMetadata(that, metadataFolder); }; // TODO: Update to pull from a "solutions codex maleficarum" or other globally available single source of truth. gpii.ontologyHandler.loadOntologyMetadata = function (that, metadataFolder) { var metadataList = fs.readdirSync(metadataFolder); fluid.each(metadataList, function (filename) { var fullPath = metadataFolder + "/" + filename; // Skip any directories we encounter: if (fs.statSync(fullPath).isDirectory()) { return; } fluid.log("ontologyHandler Loading filename " + filename); var metadata = fs.readFileSync(fullPath, "utf8"); // get the ontology name by removing the extension of the filename var ontologyName = path.parse(filename).name; that.ontologyMetadata[ontologyName] = JSON5.parse(metadata); }); }; /** * Sanitizes the "contexts:" sections and the outer metadata section by removing entries for which * * Arrays are empty * * Values are undefined * * @param {Object} prefs - The preferences to sanitize. * @return {Object} The sanitized settings (according to above definition). */ gpii.ontologyHandler.sanitizePreferencesSet = function (prefs) { fluid.each(prefs.contexts, function (prefsSet) { fluid.each(prefsSet, function (val, key) { if (val === undefined || (Array.isArray(val) && val.length === 0)) { delete prefsSet[key]; } }); }); if (prefs.metadata === undefined || (Array.isArray(prefs.metadata) && prefs.metadata.length === 0)) { delete prefs.metadata; } return prefs; }; /** * The current format for naming ontology mapping files is source-target. * * @param {String} ontologyMapping - Mapping in source-target format. * @return {Object} Object with source and target keys with ontology names. */ gpii.ontologyHandler.parseOntologyMapping = function (ontologyMapping) { var onts = ontologyMapping.split("-"); return { source: onts[0], target: onts[1] }; }; /** * Reads the files (or URLs) that contains the ontology transformation specs and stores it in * the ontologyTransformSpecs propery of the gpii.ontologyHandler object * * @param {Object} that - The gpii.ontologyHandler object. Should at minimum contain ontologySourceURL * and ontologyTransformSpecs properties. * @param {String} ontologyDir - Path to folder containing the ontology transformation specs. * @param {Array} ontologyTransformsList - A list of available ontology transformations. */ gpii.ontologyHandler.loadOntologyTransformSpecs = function (that, ontologyDir, ontologyTransformsList) { var transformSpecs = {}; fluid.each(ontologyTransformsList, function (filename) { fluid.log("ontologyHandler loading transformSpec filename " + filename); var transformSpec = fs.readFileSync(ontologyDir + "/" + filename, "utf8"); // get the ontology name by removing the extension of the filename var ontologyPair = path.parse(filename).name; transformSpecs[ontologyPair] = JSON5.parse(transformSpec); }); that.ontologyTransformSpecs = transformSpecs; // Generate Ontology Routes var edges = []; fluid.each(transformSpecs, function (spec, key) { // The transforms are bidirectional so we add both directed edges var mapping = gpii.ontologyHandler.parseOntologyMapping(key); edges.push(mapping); edges.push({ source: mapping.target, target: mapping.source }); }); that.ontologyTransformRoutes = gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction(edges); }; /** * Implementation of the Floyd-Warshall algorithm to determine the shortest * paths between a set of named edges. This is mostly generic, but currently * used in generating a path between multiple linked ontologies. * https://en.wikipedia.org/wiki/Floyd–Warshall_algorithm#Path_reconstruction * Currently assumes that all edges are weight 1. * * @param {Array} edges - An array of arrays of length 2, consisting of the named * edges such as [['A','B'], ['B','C']]. * @return {Object} An object containing the necessary structures to resolve * paths using the floydWarshallPath function. Contains 4 members: * vertices (Object) key/value pair where key is the vertex name, and value * is an integer to be used in the algorithm marking it as a vertex. * For the ontology path use case, the vertex is the name of the * ontology. * numVertices (Integer) Convenience variable for the total number of vertices(ontologies). * dist (2D Array) dist and next are matrices used in the algorithm, see * documentation link above for algorithm details. * next (2D Array) see dist above. */ gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction = function ( edges ) { var data = {}; gpii.ontologyHandler.floydWarshall.algorithmInitVertices(data, edges); gpii.ontologyHandler.floydWarshall.algorithmInitMatrices(data, edges); gpii.ontologyHandler.floydWarshall.algorithmCore(data.dist, data.next, data.numVertices); return data; }; /** * Internal Use only * Function used in gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction * to initialize the mapping of vertices named by ontology to integers used in * the algorithm. Does not a return a value, operates directly on the data structure. * * @param {Object} data - Data structure used in main function. * @param {Array} edges - List of edges where each is an Object contains a directed source and * target parameter. */ gpii.ontologyHandler.floydWarshall.algorithmInitVertices = function (data, edges) { data.vertices = {}; var record = { hash: data.vertices, count: 0 }; fluid.each(edges, function (edge) { gpii.ontologyHandler.floydWarshall.incrementingHashAppend(record, edge.source); gpii.ontologyHandler.floydWarshall.incrementingHashAppend(record, edge.target); }); data.numVertices = record.count; }; /** * Fill a hashmap as if it were a set, assigning each new unique entry a * sequential integer value. A datastructure for each item is passed in that includes * the hash we are incrementing and the current size. Does not return a value, operates directly on the hash. * * @param {Object} record - A record containing two items, "hash", which is the current hashmap * we are filling, and count, the current size of hash. * example: { hash: ourKeyedObject, count: 3 } * @param {Object} item - The next value to add to the set. If this item is already * in the set, the existing value for it will be unchanged. */ gpii.ontologyHandler.floydWarshall.incrementingHashAppend = function (record, item) { if (!record.hash[item]) { record.hash[item] = record.count; record.count += 1; } }; /** * Internal Use only * Function used in gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction * to initialize the core matrices used in the algorithm, next and dist. Does not a return a value, operates * directly on the data structure. * * @param {Object} data - Data structure used in main function. * @param {Array} edges List of edges where each is an Object contains a directed source and * target parameter. */ gpii.ontologyHandler.floydWarshall.algorithmInitMatrices = function (data, edges) { // Create |V|x|V| dist and next arrays and initialize to Infinity and null data.dist = fluid.generate(data.numVertices, function () { return fluid.generate(data.numVertices, Infinity, false); }, true); data.next = fluid.generate(data.numVertices, function () { return fluid.generate(data.numVertices, null, false); }, true); fluid.each(edges, function (edge) { var u = data.vertices[edge.source], v = data.vertices[edge.target]; data.dist[u][v] = 1; // All weights currently 1 data.next[u][v] = v; }); }; /** * Internal Use only * Function used in gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction * to perform the core algorithm of Floyd-Warshall. Does not a return a value, operates directly on the dist and * next matrices. * * @param {Array<Array>} dist - See algorithm documentation. * @param {Array<Array>} next - See algorithm documentation. * @param {Integer} numVertices - Total number of vertices. */ gpii.ontologyHandler.floydWarshall.algorithmCore = function (dist, next, numVertices) { // Standard Floyd-Warshall implementation for (var k = 0; k < numVertices; k++) { for (var i = 0; i < numVertices; i++) { for (var j = 0; j < numVertices; j++) { if (dist[i][k] + dist[k][j] < dist[i][j]) { dist[i][j] = dist[i][k] + dist[k][j]; next[i][j] = next[i][k]; } } } } }; /** * Uses the data generated in floydWarshall.fullAlgorithmWithReconstruction to generate the shortest path as an * array of vertex names. * * @param {Object} data - The data structure returned from floydWarshall.fullAlgorithmWithReconstruction. * @param {String} v1 - The starting vertex. * @param {String} v2 - The ending vertex. * @return {Array} An array of vertices for the shortest path, or an empty array if the path does not exist. */ gpii.ontologyHandler.floydWarshallPath = function ( data, v1, v2 ) { if (data.vertices[v1] === undefined || data.vertices[v2] === undefined) { return []; } var path = [], u = data.vertices[v1], v = data.vertices[v2]; if (data.next[u][v] === null) { return path; } path.push(v1); while (u !== v) { u = data.next[u][v]; path.push(fluid.keyForValue(data.vertices, u)); } return path; }; /** * Function for getting a transformation spec translating between ontologies. * * If the desired transformation spec from the requested ontology to the requested ontology * can be produced, return it. Else return undefined. * * @param {Object} transformSpecs - With all the available transformationSpecs. It should be keyed by: * "<from>-<to>" values (where <from> and <to> are replaced by the keys of ontologies) and * value should be the actual transformationSpec. * @param {String} from - The key of the ontology to transform from. * @param {String} to - The key of the ontology to transform to. * @return {Object} Transformation spec for translating the from ontology defined by param 'from' to * ontology defined by param 'from'. */ gpii.ontologyHandler.getTransformSpec = function (transformSpecs, from, to) { var fromTo = from + "-" + to; if (transformSpecs[fromTo] !== undefined) { return transformSpecs[fromTo]; } var toFrom = to + "-" + from; return (transformSpecs[toFrom] === undefined) ? undefined : fluid.model.transform.invertConfiguration(transformSpecs[toFrom]); }; /** * Function to collect all paths in the given object that has a certain value and return an * array of these paths as el-paths (strings). This is useful when transforming metadata and * we need to translate from an el-path in one ontology into an el-path in another ontology. * * The function is recursive and the 'target' parameter should be an array that will be * populated by the found paths. * * @param {Object} obj - To search through. * @param {Object} search - The value to search for. Whenever this value is found, an el-path string leading to * this location is saved to the target array. * @param {Array} target - It will be modified by this function to contain the el-paths that contain the * 'search' value. * @param {Array} pathArray - Is used by the function to keep track of the current path. Should generally * not be supplied to this function unless one wishes to have the paths that populate the * the target being prefixed by some path. * @return {undefined} - rather the array of collection will be built in the @target parameter. */ gpii.ontologyHandler.collectPaths = function (obj, search, target, pathArray) { if (!(pathArray instanceof Array)) { pathArray = []; } fluid.each(obj, function (entry, key) { pathArray.push(key); if (entry === search) { var path = fluid.pathUtil.composeSegments.apply(null, pathArray); target.push(path); pathArray.pop(key); return; } if (!fluid.isPrimitive(entry)) { gpii.ontologyHandler.collectPaths(entry, search, target, pathArray); } pathArray.pop(key); }); }; /** * Function to translate the metadata section of a preferences document - more specifically, * the scope section of a metadata part of a preferences document. For individual values * they will be translated as expected (i.e. by a lookup in the transformation rules). In case * of wildcard characters, these are _only_ supported at the end of a string. A string containing * only the wildcard ("*") will be kept as it, as this is globally applicable across * ontologies. For other wildcards (eg. "display.*") they will be exploded when requested in a * different ontology than they're specified in. So for example, a metadata section with a scope * of "display.*" would first be exploded to all the preferences under the display object * and then each of these would be transformed to the requested ontology. * * @param {Array} metadata - The metadata section to be transformed * @param {Object} transformSpec - The transformation rules to be applied to the scopes * @return {Object} The transformed metadata section */ gpii.ontologyHandler.transformMetadata = function (metadata, transformSpec) { if (metadata === undefined) { return undefined; } var togo = []; fluid.each(metadata, function (entry) { // if metadata section is application priority, we don't want to have the // scope ontologized. We want to keep the flat-style application ID if (entry.type === "priority") { togo.push(entry); return; } var tmpEntry = fluid.copy(entry), toTransform = {}, transformed, target; tmpEntry.scope = []; fluid.each(entry.scope, function (preference) { if (preference === "*") { // ontology independent wildcard - leave as is tmpEntry.scope.push(preference); return; } if (fluid.model.transform.hasWildcard(preference)) { // explode wildcard preference to paths, and set all of these in toTransform var paths = fluid.model.transform.collectInputPaths(transformSpec); var prefix = preference.substring(0, preference.indexOf("*")); for (var i in paths) { var path = paths[i]; // each matching path should be added to toTransform for trannsform if (path.indexOf(prefix) === 0) { fluid.set(toTransform, path, true, fluid.model.escapedSetConfig); } } return; } fluid.set(toTransform, preference, true, fluid.model.escapedSetConfig); }); // transform to new ontology and collect all paths with 'true' value - these equal // the settings from the original scope array transformed = fluid.model.transformWithRules(toTransform, transformSpec); target = []; gpii.ontologyHandler.collectPaths(transformed, true, target); tmpEntry.scope = tmpEntry.scope.concat(target); togo.push(tmpEntry); }); return togo; }; /** * Takes a set of preferences in some ontology X and transforms they into some ontology Y. Note * that the preferences given should NOT be keyed by an ontology ID. * * @param {Component} that - An instance of gpii.ontologyHandler component. * @param {Object} prefs - The preference set to be translated into a different ontology. The preference set * passed should be in the ontology specified by the 'fromView' parameter and NOT keyed by * an ontology ID. * @param {String} fromView - The ontology in which the preference set given in the 'prefs' parameter is formatted. * @param {String} toView - The ontology to which the preference set should be transformed. * @return {Object} The NP transformed into the ontology requested via the 'toView' parameter. If no * valid transformation to that ontology was found, an empty preference set is returned. */ gpii.ontologyHandler.prefsToOntology = function (that, prefs, fromView, toView) { var transformSpec, transformed = { name: prefs.name, contexts: prefs.contexts, metadata: prefs.metadata }; // if we're in same ontology, return as is if (fromView === toView) { return prefs; } // else get the appropriate shortest route between ontologies. Return {} if none found var transformSpecRoute = gpii.ontologyHandler.floydWarshallPath(that.ontologyTransformRoutes, fromView, toView); if (transformSpecRoute.length === 0) { return {}; } // We are iterating through the array of specs by creating the dashed filenames. // e.g transformSpecRoute is ['A', 'B', 'C'] so we would look up spec files // 'A-B' and 'B-C' for (var i = 0; i < transformSpecRoute.length - 1; i++) { transformSpec = gpii.ontologyHandler.getTransformSpec(that.ontologyTransformSpecs, transformSpecRoute[i], transformSpecRoute[i + 1]); if (transformSpec === undefined) { return {}; } var prefsSetTransform = gpii.ontologyHandler.makePrefsSetTransform(transformSpec); transformed.contexts = fluid.transform(transformed.contexts, prefsSetTransform); // translate the preferences set's independent metadata block: transformed.metadata = gpii.ontologyHandler.transformMetadata(transformed.metadata, transformSpec); } return gpii.ontologyHandler.sanitizePreferencesSet(transformed); }; /** * Creates a transform function to be used with fluid.transform to take a * set of preferences from the "contexts:" block and translate them to another * ontology. * * @param {Object} transformSpec - The transform spec used to go between ontologies. * @return {Function} A transform function for the conversion suitable to * use in fluid.transform. */ gpii.ontologyHandler.makePrefsSetTransform = function (transformSpec) { return function (prefsSet) { // copy values directly or transform - depending on key return fluid.transform(prefsSet, function (val, key) { var rule = gpii.ontologyHandler.contextBlocks[key]; return rule === null ? val : fluid.invokeGlobalFunction(rule, [val, transformSpec]); }); }; }; // Helper object for prefsToOntology function gpii.ontologyHandler.contextBlocks = { name: null, priority: null, preferences: "fluid.model.transformWithRules", metadata: "gpii.ontologyHandler.transformMetadata" }; /** * Function to merge two preference sets. * * Takes two (non-raw) prefs sets, merges them and returns the resulting set. One cannot just * use fluid.extend, as this would override array entries in the metadata section, so a more * informed merge is required. The preference sets are expected to be in the same ontology * and should not be keyed by ontology. * * @param {Object} master - The first preference set to be merged. * @param {Object} secondary - The second preference set to be merged. * @return {Object} A merged preference set (non-raw). */ gpii.ontologyHandler.mergePrefsSets = function (master, secondary) { master = fluid.copy(master); secondary = fluid.copy(secondary); master.name = master.name || secondary.name; fluid.each(secondary.contexts, function (prefsSet, prefsSetId) { var mprefsSet = master.contexts[prefsSetId]; // if not in master, just copy over from secondary: if (!mprefsSet) { master.contexts[prefsSetId] = prefsSet; } else { // preferences and metadata should be the only things varying fluid.extend(true, mprefsSet.preferences, prefsSet.preferences); mprefsSet.metadata = fluid.makeArray(mprefsSet.metadata).concat(fluid.makeArray(prefsSet.metadata)); } }); // merge outer metadata block master.metadata = fluid.makeArray(master.metadata).concat(fluid.makeArray(secondary.metadata)); return master; }; /** * Function to take a raw preference set and transform it into a desired ontology (incl. doing * the required transformations for each of the preferences. metadata, etc). This function does * not have any side-effects * * @param {Object} that - An instance of gpii.ontologyHandler object. * @param {Object} rawPrefs - The raw preferences as can be found on the preferences server - NOT * expected keyed by 'preferences'. * @param {String} toView - The ontology to translate the preference set into. * @return {Object} Preferences in the ontology given in 'toView' parameter. This includes the preferences, * metadata, etc., resulting from transforming all compatible preferences from different ontologies. */ gpii.ontologyHandler.rawPrefsToOntology = function (that, rawPrefs, toView) { var togo = { contexts: {} }; fluid.each(rawPrefs, function (fromContent, fromView) { var transformed = gpii.ontologyHandler.prefsToOntology(that, fromContent, fromView, toView); togo = gpii.ontologyHandler.mergePrefsSets(togo, transformed); }); return gpii.ontologyHandler.sanitizePreferencesSet(togo); }; /** * Takes a preference set X in a particular ontology and removes all settings in the various * ontologies of the raw preference set (Y) that are duplicates of the settings in the X * preference set. The result is a raw preference set that contains the preferences of X in * it's ontology, and no duplicates of settings of the X settings in the other ontologies. Note * that the preferneces given will overwrite the entire entry of that ontology in the raw * preference set provided (i.e. it wont get merged with any settings already existing in that * ontology) * * In other words: if any of the preferences that are being added are present in another * ontology, that entry should be removed from the other ontology, to avoid duplication of the * same term in different ontologies. * * @param {Object} that - An instance of gpii.ontologyHandler component. * @param {Object} prefs - A preference set in some ontology. * @param {String} prefsView - The format of the preference set given in the 'prefs' parameter. * @param {Object} rawPrefs - The raw preference set to be modified. * @return {Object} A raw preference set that contains the settings of 'prefs' in the ontology * 'prefsView', and has no settings in any of the other ontologies that can be transformed * (or created/inferred) from the settings of 'prefs'. */ gpii.ontologyHandler.addPrefsToRawPrefs = function (that, prefs, prefsView, rawPrefs) { var togo = fluid.copy(rawPrefs); // on an update, the consumer is responsible for sending _ALL_ relevant preferences in an // ontology, so we can safely replace the existing preferences in that ontology togo[prefsView] = prefs; // for each of the remaining ontologies in the raw preferences, check if there are any // preferences that are duplicate and remove them if that is the case: fluid.each(togo, function (existingPrefs, existingView) { if (existingView === prefsView) { return; } // TODO - GPII-848: The preference merging needs to be improved once the user has the // ability to specifically 'unset' settings in the PSP/PMT - i.e. decide that they do not // want eg. font-size defined anymore. // transform settings from the new preference set (prefsView) into the ontology // we're current looking at (i.e. 'existingView') var transformed = gpii.ontologyHandler.prefsToOntology(that, prefs, prefsView, existingView); // dig down to each of the prefs sets: var contexts = transformed.contexts; fluid.each(contexts, function (prefsSet, prefsSetName) { if (prefsSet.preferences && existingPrefs.contexts[prefsSetName] && existingPrefs.contexts[prefsSetName].preferences) { var filteredPrefs = gpii.ontologyHandler.utils.filterPrefs( existingPrefs.contexts[prefsSetName].preferences, prefsSet.preferences); togo[existingView].contexts[prefsSetName].preferences = filteredPrefs; } }); }); // clean up prefs sets for each ontology return fluid.transform(togo, gpii.ontologyHandler.sanitizePreferencesSet); }; })();