jsmf-magellan
Version:
Navigation module for jsmf
273 lines (243 loc) • 10.6 kB
JavaScript
/**
* @license
* ©2015-2016 Luxembourg Institute of Science and Technology All Rights Reserved
* JavaScript Modelling Framework (JSMF)
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @author J.S. Sottet
* @author N. Biri
* @author A. Vagner
*/
const _ = require ('lodash')
/**
* Crawl crawls la whole JSMF model from a given entry point
*
* @param {object} searchParameters - The definition of how the model is crawled, the following object properties are inspected:
* @param {Function} [searchParameters.predicate=_.constant(true)] - A predicate (a function that takes an object as parameter) that must be fullfilled by an object to be part of the result. If undefined, all the objects are accepted
* @param {Number} [searchParameters.depth=-1] - the number of references to be followed befor we stop crawling, if we don't want to limit crawling, use -1.
* @param {Function} [searchParameters.followIf=_.constant(true)] - A function that take an object and a reference as parameters, if the function is evaluate to true, we follow this reference, otherwise, we stop crawling this branch. If undefined, all the references are followed.
* @param {boolean} [searchParameters.stopOnfirst=false] - Set if we continue to crawl the model when the expected predicate is found.
* @param {boolean} [searchParameters.includeRoot=true] - include the entrypoint in the result.
* @param {object} entrypoint - The entrypoint object to crawl the model.
* @returns {List} The elements that fullfill the searchParameters
* See unit tests for examples.
*/
function crawl(searchParameters, entrypoint) {
const predicate = searchParameters['predicate'] || _.constant(true)
let depth = searchParameters['depth']
if (depth === undefined) { depth = -1 }
const propertyFilter = searchParameters['followIf'] || _.constant(true)
let includeRoot = searchParameters['includeRoot']
if (includeRoot === undefined) { includeRoot = true }
let stopOnFirst = searchParameters['stopOnFirst']
if (stopOnFirst === undefined) { stopOnFirst = false }
const startingNodes = includeRoot
? [crawlEntry(entrypoint, depth)]
: _.map(nodeChildren(propertyFilter, entrypoint), x => crawlEntry(x, nextDepth(depth)))
return _crawl(predicate, propertyFilter, stopOnFirst, startingNodes).result
}
function nextDepth(depth) {
return depth > 0 ? depth - 1 : depth
}
function _crawl(predicate, propertyFilter, stopOnFirst, entrypoints) {
const ctx = {visited: new Set(), result: []}
while (!(_.isEmpty(entrypoints))) {
const current = entrypoints.pop()
const entrypoint = current.elem
const depth = current.depth
let children = []
let found = false
if (entrypoint !== undefined && !ctx.visited.has(entrypoint)) {
ctx.visited.add(entrypoint)
found = predicate(entrypoint)
if (found) {
ctx.result.push(entrypoint)
if (stopOnFirst) { return ctx }
}
if (depth !== 0) {
children = nodeChildren(propertyFilter, entrypoint)
}
const newDepth = nextDepth(depth)
children = _.map(children, x => crawlEntry(x, newDepth))
}
entrypoints = entrypoints.concat(children)
}
return ctx
}
function nodeChildren(filter, entrypoint) {
const refs = entrypoint.conformsTo().getAllReferences()
return _(refs).map((v, ref) => filter(entrypoint, ref) ? entrypoint[ref] : [])
.flatten()
.value()
}
function crawlEntry(elem, depth) {
return {elem, depth}
}
/**
* Get all the modelingelements from a model that belongs to a class (according to their inheritance chain)
* @param {Class} cls - The class we are looking for
* @param {Model} model - The inspected model.
* @param {boolean} [strict=false] - If strict is false, seek for instances of the clas or of any of its subclass. Otherwise, seek only exact instances of the class.
*/
function allInstancesFromModel (cls, model, strict) {
const me = _.get(model, ['referenceModel', 'modellingElements'])
if (_.isEmpty(me)) {
const os = _(model.modellingElements).values().flatten().value()
if (!strict) {
return _.filter(os,
x => _(x.conformsTo().getInheritanceChain())
.map('__name')
.includes(cls.__name))
} else {
return _.filter(os, x=> x.conformsTo().__name === cls.__name)
}
} else if (!strict) {
const clss = _(me).values().flatten().value()
return _(clss).filter( x => (x.getInheritanceChain !== undefined)
&& _.includes(x.getInheritanceChain(), cls))
.map('__name')
.map(x => model.modellingElements[x] || [])
.flatten()
.value()
} else {
return me[cls.__name]
}
}
/**
* Get all the modelingelements from a model that satisfies a predicate
* @param {Function} predicate - A predicate (a function that takes an object as parameter) that must be fullfilled by an object to be part of the result.
* @param {Model} model - The inspected model.
* @returns {List} The elements of the model that verifies the predicate function
*/
function filterModelElements (predicate, model) {
return _(model.modellingElements).values()
.flatten()
.filter(x => predicate(x))
.value()
}
/**
* Get the elements down a given path from a given entrypoint of a model.
* @param {object} searchParameters - The parameters of the search. The following properties are inspected:
* @param {List} path - The path to follow. A path is a list of reference names that must be followed. The last element can be an attribute name. If no value is given, the default value is the empty list
* @param {Function} [searchParameters.predicate=_.constant(true)] - A predicate (a function that takes an object as parameter) that must be fullfilled by an object to be part of the result. If undefined, all the objects are accepted
* @param {boolean} [searchParameters.targetOnly=true] - if true, we return only the objects at the end of the path otherwise, we also take objects we pass through during the search. Default value is 'true'.
* @param {Symbol} - [searchParameters.searchMethod=DFS_All] - The searchMethod used to crawl the model (either {DFS_All}, {DFS_First}, {BFS_All}, or {BFS_First}).
* @returns {List} The elements that fullfill the searchParameters
*/
function follow(searchParameters, entrypoint) {
const path = searchParameters['path'] || []
path.reverse()
const predicate = searchParameters['predicate'] || _.constant(true)
let targetOnly = searchParameters['targetOnly']
if (targetOnly === undefined) { targetOnly = true }
const method = searchParameters['searchMethod'] || DFS_All
const entrypoints = [followEntry(entrypoint, path)]
return _follow(predicate, method, targetOnly, entrypoints)
}
function followEntry(elem, path) {
return {elem, path}
}
function _follow(predicate, method, targetOnly, entrypoints) {
const acc = []
while (!(_.isEmpty(entrypoints))) {
const current = entrypoints.pop()
const entrypoint = current.elem
const path = current.path
if ((!targetOnly || _.isEmpty(path)) && predicate(entrypoint)) {
acc.push(entrypoint)
if (stopOnFirst(method)) { return acc }
}
if (!(_.isEmpty(path))) {
const pathElement = path.pop()
if (_.isString(pathElement)) {
let values = entrypoint[pathElement]
if (values === undefined) {
throw new Error(`Unsuppported method ${pathElement} for object ${entrypoint}`)
}
values = _.map(values, x => followEntry(x, path))
entrypoints = isDFS(method)
? entrypoints.concat(values)
: values.concat(entrypoints)
} else if (pathElement instanceof Function) {
if (pathElement(entrypoint)) {
entrypoints.push(followEntry(entrypoint, path))
}
} else {
throw new Error(`invalid path element ${pathElement}`)
}
}
}
return acc
}
/*********************
* Predicate helpers *
*********************/
/**
* hasClass returns a function that checks if a given object belongs to the given JSMF Class.
* @param {Class} cls - The expected Class.
*/
function hasClass(cls) {
return (x => _.includes(x.conformsTo().getInheritanceChain(), cls))
}
/**************************
* PropertyFilter helpers *
**************************/
/**
* A helper for the construction of the propertyFilter for @{allInstancesFromObject} and @{getObjectsFromObject}.
* for a given clas, follows only the references provided in the corresponding map entry.
*/
function referenceMap(x) {
return ((e, ref) => {
const hierarchy = e.conformsTo().getInheritanceChain()
return _.some(hierarchy, c => _.includes(x[c.__name], ref))
})
}
/******************
* Search Methods *
******************/
/**
* - DFS_All: Deep First Search, get all the elements that match the predicate.
*/
const DFS_All = Symbol('DFS_All')
/**
* BFS_All: Breadth First Search, get all the elements that match the predicate.
*/
const BFS_All = Symbol('BFS_All')
/**
* DFS_First: Deep First Search, get the first element that matches the predicate.
*/
const DFS_First = Symbol('DFS_First')
/**
* BFS_First: Breadth First Search, get the first element that matches the predicate.
*/
const BFS_First = Symbol('BFS_First')
function isDFS(m) {
return _.includes([DFS_All, DFS_First], m)
}
function stopOnFirst(m) {
return _.includes([DFS_First, BFS_First], m)
}
module.exports = {
crawl,
follow,
allInstancesFromModel,
filterModelElements,
DFS_All,
DFS_First,
BFS_All,
BFS_First,
hasClass,
referenceMap
}