webgme-engine
Version:
WebGME server and Client API without a GUI
659 lines (584 loc) • 26.4 kB
JavaScript
/*globals define*/
/*eslint-env node, browser*/
/**
* @author kecso / https://github.com/kecso
* @author pmeijer / https://github.com/pmeijer
*/
define(['q', 'common/core/constants'], function (Q, CONSTANTS) {
'use strict';
// Helper functions.
function loadNode(core, rootNode, nodePath) {
return core.loadByPath(rootNode, nodePath)
.then(function (node) {
if (node === null) {
throw new Error('Given nodePath does not exist "' + nodePath + '"!');
} else {
return node;
}
});
}
function loadNodes(core, node, nodePaths) {
var i,
loadPromises = [],
rootNode = core.getRoot(node);
for (i = 0; i < nodePaths.length; i += 1) {
loadPromises.push(loadNode(core, rootNode, nodePaths[i]));
}
return Q.all(loadPromises);
}
function filterPointerRules(meta) {
var result = {
pointers: {},
sets: {}
},
pointerNames = Object.keys(meta.pointers),
i;
for (i = 0; i < pointerNames.length; i += 1) {
if (meta.pointers[pointerNames[i]].max === 1) {
// These are single target pointers (e.g. connections).
result.pointers[pointerNames[i]] = meta.pointers[pointerNames[i]];
} else {
// These are multi target pointer, i.e. sets.
result.sets[pointerNames[i]] = meta.pointers[pointerNames[i]];
}
}
return result;
}
function getMatchedItemIndices(core, node, items) {
var i,
metaNodes = core.getAllMetaNodes(node),
indices = [];
for (i = 0; i < items.length; i += 1) {
if (core.isTypeOf(node, metaNodes[items[i]])) {
indices.push(i);
}
}
return indices;
}
function metaNodePathToName(core, metaNodes, path) {
var name = 'Unknown';
if (metaNodes[path]) {
name = core.getAttribute(metaNodes[path], 'name');
}
return name;
}
function checkNodeTypesAndCardinality(core, node, nodes, subMetaRules, checkTypeText, singular) {
var matches = [],
metaNodes = core.getAllMetaNodes(node),
result = {
hasViolation: false,
messages: []
},
i,
j,
matchedIndices;
/*
* example subMetaRules
* {
* items: [ '/1', '/822429792/942380411' ],
* min: undefined,
* max: undefined,
* minItems: [ -1, -1 ],
* maxItems: [ -1, 4 ]
* }
*/
// Initialize the number of matches for each valid type.
matches = subMetaRules.items.map(function () {
return 0;
});
// For each node
for (i = 0; i < nodes.length; i += 1) {
// check which types it matches and
matchedIndices = getMatchedItemIndices(core, nodes[i], subMetaRules.items);
if (matchedIndices.length === 0) {
result.hasViolation = true;
result.messages.push('Illegal node "' + core.getAttribute(nodes[i], 'name') + '" [' +
core.getPath(nodes[i]) + '] ' + (singular ? 'as ' : 'among ') + checkTypeText + '.');
} else {
// increase the counter for each type it matches.
for (j = 0; j < matchedIndices.length; j += 1) {
matches[matchedIndices[j]] += 1;
}
}
}
for (i = 0; i < subMetaRules.items.length; i += 1) {
if (subMetaRules.minItems[i] > -1 && subMetaRules.minItems[i] > matches[i]) {
result.hasViolation = true;
result.messages.push('Fewer ' + checkTypeText + ' (' +
metaNodePathToName(core, metaNodes, subMetaRules.items[i]) + ') than needed - there should be ' +
subMetaRules.minItems[i] + ' but only ' + matches[i] + ' found.');
} else if (subMetaRules.maxItems[i] > -1 && subMetaRules.maxItems[i] < matches[i]) {
result.hasViolation = true;
result.messages.push('More ' + checkTypeText + '(' +
metaNodePathToName(core, metaNodes, subMetaRules.items[i]) + ') than allowed - there can only be ' +
subMetaRules.maxItems[i] + ' but ' + matches[i] + ' found.');
}
}
return result;
}
// Checker functions for pointers, sets, containment and attributes.
function checkPointerRules(meta, core, node, callback) {
var result = {
hasViolation: false,
messages: []
},
metaPointers = filterPointerRules(meta).pointers,
checkPromises = [],
pointerNames = core.getPointerNames(node);
checkPromises = pointerNames.map(function (pointerName) {
var metaPointer = metaPointers[pointerName],
pointerPath,
pointerPaths = [];
if (!metaPointer) {
if (pointerName === 'base') {
return {hasViolation: false};
} else {
return Q({
hasViolation: true,
messages: ['Illegal pointer "' + pointerName + '".']
});
}
} else {
pointerPath = core.getPointerPath(node, pointerName);
if (pointerPath !== null) {
pointerPaths.push(pointerPath);
}
return loadNodes(core, node, pointerPaths)
.then(function (nodes) {
return checkNodeTypesAndCardinality(core, node, nodes, metaPointer,
'"' + pointerName + '" target', true);
});
}
});
return Q.all(checkPromises)
.then(function (results) {
results.forEach(function (res) {
if (res.hasViolation) {
result.hasViolation = true;
result.messages = result.messages.concat(res.messages);
}
});
return result;
}).nodeify(callback);
}
function checkSetRules(meta, core, node, callback) {
var result = {
hasViolation: false,
messages: []
},
metaSets = filterPointerRules(meta).sets,
checkPromises = [],
setNames = core.getSetNames(node);
checkPromises = setNames.map(function (setName) {
var metaSet = metaSets[setName],
memberPaths;
if (!metaSet) {
if (core.getValidAspectNames(node).indexOf(setName) > -1) {
// TODO: Should the Aspects be checked too?
return Q({
hasViolation: false
});
} else {
var crossCuts = core.getRegistry(node, 'CrossCuts') || [],
i;
// The 'CrossCuts' is a constant from client/js/RegistryKeys.js
for (i = 0; i < crossCuts.length; i += 1) {
if (crossCuts[i].SetID === setName) {
i = -1;
break;
}
}
if (i === -1) {
// TODO: Should the CrossCuts be checked too?
return Q({
hasViolation: false
});
} else {
return Q({
hasViolation: true,
messages: ['Illegal set "' + setName + '".']
});
}
}
} else {
memberPaths = core.getMemberPaths(node, setName);
return loadNodes(core, node, memberPaths)
.then(function (nodes) {
return checkNodeTypesAndCardinality(core, node, nodes, metaSet, '"' + setName + '"-members');
});
}
});
return Q.all(checkPromises)
.then(function (results) {
results.forEach(function (res) {
if (res.hasViolation) {
result.hasViolation = true;
result.messages = result.messages.concat(res.messages);
}
});
return result;
}).nodeify(callback);
}
function checkChildrenRules(meta, core, node, callback) {
return core.loadChildren(node)
.then(function (nodes) {
return checkNodeTypesAndCardinality(core, node, nodes, meta.children, 'children');
})
.nodeify(callback);
}
function checkAttributeRules(meta, core, node) {
var result = {
hasViolation: false,
messages: []
},
names = core.getAttributeNames(node),
validNames = core.getValidAttributeNames(node),
ownVal,
i;
function checkValidValue(attrName) {
try {
if (!core.isValidAttributeValueOf(node, attrName, core.getAttribute(node, attrName))) {
result.hasViolation = true;
result.messages.push('Attribute "' + attrName + '" has invalid value "' +
core.getAttribute(node, attrName) + '".');
}
} catch (e) {
if (e.message.indexOf('Invalid regular expression') > -1) {
result.messages.push('Invalid regular expression defined for attribute "' + attrName + '"!');
result.hasViolation = true;
} else {
throw e;
}
}
}
for (i = 0; i < names.length; i++) {
if (validNames.indexOf(names[i]) !== -1) {
if (meta.attributes[names[i]].readonly === true) {
ownVal = core.getOwnAttribute(node, names[i]);
if (ownVal !== undefined && core.isMetaNode(node) === false) {
result.messages.push('Read-only attribute "' + names[i] +
'" value has been set for a non-meta node!');
result.hasViolation = true;
} else if (core.isMetaNode(node)) {
checkValidValue(names[i]);
}
} else {
checkValidValue(names[i]);
}
} else {
result.hasViolation = true;
result.messages.push('Illegal attribute "' + names[i] + '".');
}
}
return Q(result);
}
/**
*
* @param core
* @param node
* @param [callback]
* @returns {Q.Promise}
*/
function checkNode(core, node, callback) {
var result = {
hasViolation: false,
messages: [],
message: ''
},
meta;
if (core.getPath(node) === '' || core.isLibraryRoot(node)) {
// Do not check the meta-rules for the root-node or library-roots.
return Q(result);
}
meta = core.getJsonMeta(node);
return Q.all([
checkPointerRules(meta, core, node),
checkSetRules(meta, core, node),
checkChildrenRules(meta, core, node),
checkAttributeRules(meta, core, node)
])
.then(function (results) {
var i;
for (i = 0; i < results.length; i += 1) {
if (results[i].hasViolation === true) {
result.hasViolation = true;
result.messages = result.messages.concat(results[i].messages);
}
}
result.message = result.messages.join(' ');
return result;
})
.nodeify(callback);
}
/**
* Checks that the meta-nodes and their definitions are consistent w.r.t.
* - Meta name collisions.
* - Referencing nodes outside of the meta.
* - Duplicate definitions from mixins.
* - Collisions between set names and pointers/aspects
* - Invalid regular expression for attributes
* - Invalid min/max for attributes
* - Invalid set/pointer/attribute/aspect/constraint names
* @param core
* @param node - any node in tree to be checked
*/
function checkMetaConsistency(core, node) {
var metaNodes = core.getAllMetaNodes(node),
names = {},
result = [],
isPointer,
i,
key,
path,
metaNode,
metaName,
setNames,
pointerNames,
aspectNames,
childPaths,
ownMetaJson;
function isTypeOfAny(node, paths) {
var i,
metaNode;
for (i = 0; i < paths.length; i += 1) {
metaNode = metaNodes[paths[i]];
if (metaNode && core.isTypeOf(node, metaNode)) {
return true;
}
}
return false;
}
function getUnderScoreError(metaName, path, key, type) {
return {
severity: 'error',
message: metaName + ' defines ' + type + ' [' + key + '] starting with an underscore ("_").',
description: 'Such relations/properties in the models are considered private and can ' +
'collied with reserved properties.',
hint: 'Remove/rename it.',
path: path,
relatedPaths: []
};
}
function getReservedNameError(metaName, path, key, type) {
return {
severity: 'error',
message: metaName + ' defines ' + type + ' [' + key + '] which is a reserved name.',
description: 'Such relations/properties in the models can lead to collisions resulting in unexpected' +
' behavior.',
hint: 'Remove/rename it.',
path: path,
relatedPaths: []
};
}
function getMixinError(mixinError) {
return {
severity: mixinError.severity,
message: mixinError.message,
description: 'Mixin violations makes it hard to see which definition is used.',
hint: mixinError.hint,
path: path,
relatedPaths: mixinError.collisionPaths || []
};
}
for (path in metaNodes) {
metaNode = metaNodes[path];
metaName = core.getFullyQualifiedName(metaNode);
ownMetaJson = core.getOwnJsonMeta(metaNode);
setNames = core.getValidSetNames(metaNode);
pointerNames = core.getValidPointerNames(metaNode);
aspectNames = core.getValidAspectNames(metaNode);
childPaths = core.getValidChildrenPaths(metaNode);
//Patch the ownMetaJson
ownMetaJson.attributes = ownMetaJson.attributes || {};
ownMetaJson.children = ownMetaJson.children || {};
ownMetaJson.pointers = ownMetaJson.pointers || {};
ownMetaJson.aspects = ownMetaJson.aspects || {};
ownMetaJson.constraints = ownMetaJson.constraints || {};
// Check for name duplication.
if (typeof names[metaName] === 'string') {
result.push({
severity: 'error',
message: 'Duplicate name among meta-nodes [' + metaName + ']',
description: 'Non-unique meta names makes it hard to reason about the meta-model',
hint: 'Rename one of the objects',
path: path,
relatedPaths: [names[metaName]]
});
} else {
names[metaName] = path;
}
// Get the mixin errors.
result = result.concat(core.getMixinErrors(metaNode).map(getMixinError));
if (ownMetaJson.children.items) {
for (i = 0; i < ownMetaJson.children.items.length; i += 1) {
if (!metaNodes[ownMetaJson.children.items[i]]) {
result.push({
severity: 'error',
message: metaName + ' defines containment of a node that is not part of the meta.',
description: 'All defined meta-relations should be between meta-nodes.',
hint: 'Locate the related node, add it to the meta and remove the containment definition.',
path: path,
relatedPaths: [ownMetaJson.children.items[i]]
});
}
}
}
for (key in ownMetaJson.pointers) {
isPointer = ownMetaJson.pointers[key].max === 1;
for (i = 0; i < ownMetaJson.pointers[key].items.length; i += 1) {
if (!metaNodes[ownMetaJson.pointers[key].items[i]]) {
result.push({
severity: 'error',
message: metaName + ' defines a ' + (isPointer ? 'pointer' : 'set') + ' [' + key + '] ' +
'where the ' + (isPointer ? 'target' : 'member') + ' is not part of the meta.',
description: 'All defined meta-relations should be between meta-nodes.',
hint: 'Locate the related node, add it to the meta and remove the ' +
(isPointer ? 'pointer' : 'set') + ' definition.',
path: path,
relatedPaths: [ownMetaJson.pointers[key].items[i]]
});
}
}
if (isPointer) {
if (setNames.indexOf(key) > -1) {
result.push({
severity: 'error',
message: metaName + ' defines a pointer [' + key + '] colliding with a set definition.',
description: 'Pointer and set definitions share the same namespace.',
hint: 'Remove/rename one of them.',
path: path,
relatedPaths: ownMetaJson.pointers[key].items
});
}
if (key === CONSTANTS.BASE_POINTER || key === CONSTANTS.MEMBER_RELATION) {
result.push(getReservedNameError(metaName, path, key, 'a pointer'));
}
} else {
if (pointerNames.indexOf(key) > -1) {
result.push({
severity: 'error',
message: metaName + ' defines a set [' + key + '] colliding with a pointer definition.',
description: 'Pointer and set definitions share the same namespace.',
hint: 'Remove/rename one of them.',
path: path,
relatedPaths: ownMetaJson.pointers[key].items
});
}
if (aspectNames.indexOf(key) > -1) {
result.push({
severity: 'error',
message: metaName + ' defines a set [' + key + '] colliding with an aspect definition.',
description: 'Sets and aspects share the same name-space.',
hint: 'Remove/rename one of them.',
path: path,
relatedPaths: ownMetaJson.pointers[key].items
});
}
if (key === CONSTANTS.OVERLAYS_PROPERTY) {
result.push(getReservedNameError(metaName, path, key, 'a set'));
}
}
if (key[0] === '_') {
result.push(getUnderScoreError(metaName, path, key, isPointer ? 'a pointer' : 'a set'));
}
}
for (key in ownMetaJson.aspects) {
for (i = 0; i < ownMetaJson.aspects[key].length; i += 1) {
if (!metaNodes[ownMetaJson.aspects[key][i]]) {
result.push({
severity: 'error',
message: metaName + ' defines an aspect [' + key + '] where a member is not part of' +
' the meta.',
description: 'All defined meta-relations should be between meta-nodes.',
hint: 'Remove the item from the aspect.',
path: path,
relatedPaths: [ownMetaJson.aspects[key][i]]
});
} else if (isTypeOfAny(metaNodes[ownMetaJson.aspects[key][i]], childPaths) === false) {
result.push({
severity: 'error',
message: metaName + ' defines an aspect [' + key + '] where a member does not have a ' +
'containment definition.',
description: 'All defined meta-relations should be between meta-nodes.',
hint: 'Remove the item from the aspect or add a containment definition.',
path: path,
relatedPaths: [ownMetaJson.aspects[key][i]]
});
}
}
if (setNames.indexOf(key) > -1) {
result.push({
severity: 'error',
message: metaName + ' defines an aspect [' + key + '] colliding with a set definition.',
description: 'Sets and aspects share the same name-space.',
hint: 'Remove the aspect and create a new one.',
path: path,
relatedPaths: []
});
}
if (key === CONSTANTS.OVERLAYS_PROPERTY) {
result.push(getReservedNameError(metaName, path, key, 'an aspect'));
}
if (key[0] === '_') {
result.push(getUnderScoreError(metaName, path, key, 'an aspect'));
}
}
for (key in ownMetaJson.attributes) {
if (Object.hasOwn(ownMetaJson.attributes[key], 'regexp')) {
try {
new RegExp(ownMetaJson.attributes[key].regexp);
} catch (err) {
result.push({
severity: 'error',
message: metaName + ' defines an invalid regular expression for the attribute [' + key +
'], "' + ownMetaJson.attributes[key].regexp + '".',
description: 'Invalid properties can lead to unexpected results in the models.',
hint: 'Edit the regular expression for the attribute.',
path: path,
relatedPaths: []
});
}
}
if (ownMetaJson.attributes[key].type === CONSTANTS.ATTRIBUTE_TYPES.INTEGER ||
ownMetaJson.attributes[key].type === CONSTANTS.ATTRIBUTE_TYPES.FLOAT) {
if (Object.hasOwn(ownMetaJson.attributes[key], 'min') &&
typeof ownMetaJson.attributes[key].min !== 'number') {
result.push({
severity: 'error',
message: metaName + ' defines an invalid min value for the attribute [' + key +
']. The type is not a number but "' + typeof ownMetaJson.attributes[key].min + '".',
description: 'Invalid properties can lead to unexpected results in the models.',
hint: 'Edit the min value for the attribute.',
path: path,
relatedPaths: []
});
}
if (Object.hasOwn(ownMetaJson.attributes[key], 'max') &&
typeof ownMetaJson.attributes[key].max !== 'number') {
result.push({
severity: 'error',
message: metaName + ' defines an invalid max value for the attribute [' + key +
']. The type is not a number but "' + typeof ownMetaJson.attributes[key].max + '".',
description: 'Invalid properties can lead to unexpected results in the models.',
hint: 'Edit the max value for the attribute.',
path: path,
relatedPaths: []
});
}
}
// This cannot happen since _s are filtered out.
if (key[0] === '_') {
result.push(getUnderScoreError(metaName, path, key, 'an attribute'));
}
}
// for (key in ownMetaJson.constraints) {
// // Any checking on constraints?
// }
}
return result;
}
return {
checkNode: checkNode,
checkMetaConsistency: checkMetaConsistency
};
});