diff --git a/lib/util/model/ModelCloneHelper.js b/lib/util/model/ModelCloneHelper.js index 9112f854..ec828be3 100644 --- a/lib/util/model/ModelCloneHelper.js +++ b/lib/util/model/ModelCloneHelper.js @@ -2,9 +2,10 @@ var forEach = require('lodash/collection/forEach'), filter = require('lodash/collection/filter'), - isArray = require('lodash/lang/isArray'), - contains = require('lodash/collection/contains'); + any = require('lodash/collection/any'), + isArray = require('lodash/lang/isArray'); +var IGNORED_PROPERTIES = require('./ModelCloneUtils').IGNORED_PROPERTIES; function isAllowedIn(extProp, type) { var allowedIn = extProp.meta.allowedIn; @@ -17,107 +18,125 @@ function isAllowedIn(extProp, type) { return allowedIn.indexOf(type) !== -1; } - -function isAllowedIn(extProp, type) { - var allowedIn = extProp.meta.allowedIn; - - // '*' is a wildcard, which means any element is allowed to use this property - if (allowedIn.length === 1 && allowedIn[0] === '*') { - return true; - } - - return allowedIn.indexOf(type) !== -1; +function isType(element, types) { + return any(types, function(type) { + return typeof element === type; + }); } /** * A bpmn properties cloning interface * - * @param {Moddle} moddle */ -function ModelCloneHelper(moddle) { - this._moddle = moddle; -} +function ModelCloneHelper() {} module.exports = ModelCloneHelper; ModelCloneHelper.prototype.clone = function(oldElement, newElement, properties) { - var moddle = this._moddle; + this._newElement = newElement; forEach(properties, function(propName) { - var oldElementProp = oldElement.$model.properties.get(oldElement, propName), - newElementProp = newElement.$model.properties.get(newElement, propName); + var oldElementProp = oldElement.get(propName), + newElementProp = newElement.get(propName), + propDescriptor = newElement.$model.getPropertyDescriptor(newElement, propName), + name; - if (newElementProp === oldElementProp) { + // we're not interested in cloning: + // - same values from simple types + // - cloning id's + // - cloning reference elements + if (newElementProp === oldElementProp || + (propDescriptor && (propDescriptor.isId || propDescriptor.isReference))) { return; } - // if it's not an 'extensionElement' or 'documentation' just set the property - if (!(contains([ 'bpmn:extensionElements', 'bpmn:documentation' ], propName))) { - newElement.$model.properties.set(newElement, propName, oldElementProp); + // if the property is of type 'boolean', 'string', 'number' or 'null', just set it + if (isType(oldElementProp, [ 'boolean', 'string', 'number' ]) || oldElementProp === null) { + newElement.set(propName, oldElementProp); return; } - if (propName === 'bpmn:extensionElements') { - newElement.$model.properties.set(newElement, propName, moddle.create('bpmn:ExtensionElements', { values: [] })); - - forEach(oldElementProp.values, function(extElement) { - - var extProp = moddle.registry.typeMap[extElement.$type]; - - if (extProp.meta.allowedIn && isAllowedIn(extProp, newElement.$type)) { - - var newProp = this._deepClone(extElement); - - newProp.$parent = newElement.extensionElements; - - newElement.extensionElements.values.push(newProp); - } - }, this); - } else if (propName === 'bpmn:documentation') { - newElement.documentation = []; + if (isArray(oldElementProp)) { forEach(oldElementProp, function(extElement) { var newProp = this._deepClone(extElement); newProp.$parent = newElement; - newElement.documentation.push(newProp); + newElementProp.push(newProp); }, this); + } else { + name = propName.replace(/bpmn:/, ''); + + newElement[name] = this._deepClone(oldElementProp); } }, this); return newElement; }; -ModelCloneHelper.prototype._deepClone = function _deepClone(extElement) { - var newProp = extElement.$model.create(extElement.$type), - properties = filter(Object.keys(extElement), function(prop) { return prop !== '$type'; }); +ModelCloneHelper.prototype._deepClone = function _deepClone(element) { + var newElement = this._newElement; + + var newProp = element.$model.create(element.$type); + + var properties = filter(Object.keys(element), function(prop) { + var descriptor = newProp.$model.getPropertyDescriptor(newProp, prop); + + if (descriptor && (descriptor.isId || descriptor.isReference)) { + return false; + } + + // we need to make sure we don't clone certain properties + // which we cannot easily know if they hold references or not + if (IGNORED_PROPERTIES.indexOf(prop) !== -1) { + return false; + } + + // make sure we don't copy the type + return prop !== '$type'; + }); forEach(properties, function(propName) { - // check if the extElement has this property defined - if (extElement[propName] !== undefined && (extElement[propName].$type || isArray(extElement[propName]))) { + // check if the element has this property defined + if (element[propName] !== undefined && (element[propName].$type || isArray(element[propName]))) { - if (isArray(extElement[propName])) { + if (isArray(element[propName])) { newProp[propName] = []; - forEach(extElement[propName], function(property) { - var newDeepProp = this._deepClone(property); + forEach(element[propName], function(property) { + var extProp = element.$model.getTypeDescriptor(property.$type), + newDeepProp; + + // we're not going to copy undefined types + if (!extProp) { + return; + } + + // make sure only allowed extensionElements are copied + if (element.$type === 'bpmn:ExtensionElements' && + extProp.meta && extProp.meta.allowedIn && + !isAllowedIn(extProp, newElement.$type)) { + return; + } + + newDeepProp = this._deepClone(property); newDeepProp.$parent = newProp; newProp[propName].push(newDeepProp); }, this); - } else if (extElement[propName].$type) { - newProp[propName] = this._deepClone(extElement[propName]); + } else if (element[propName].$type) { + newProp[propName] = this._deepClone(element[propName]); newProp[propName].$parent = newProp; } } else { // just assign directly if it's a value - newProp[propName] = extElement[propName]; + newProp[propName] = element[propName]; } }, this); diff --git a/test/spec/util/ModelCloneHelperSpec.js b/test/spec/util/ModelCloneHelperSpec.js index dc6f11bc..b65342e4 100644 --- a/test/spec/util/ModelCloneHelperSpec.js +++ b/test/spec/util/ModelCloneHelperSpec.js @@ -117,6 +117,9 @@ describe('util/ModelCloneHelper', function() { var executionListener = serviceTask.extensionElements.values[0]; // then + expect(executionListener).to.not.equal(userTask.extensionElements.values[0]); + expect(executionListener.$type).to.equal('camunda:ExecutionListener'); + expect(executionListener.$type).to.equal('camunda:ExecutionListener'); expect(executionListener.$parent).to.equal(serviceTask.extensionElements); @@ -182,6 +185,30 @@ describe('util/ModelCloneHelper', function() { expect(newOutParam.definition.items[0].value).to.equal('${1+1}'); })); + + it('should not pass disallowed deeply nested property - connector', inject(function(moddle) { + + // given + var connector = moddle.create('camunda:Connector', { + connectorId: 'hello_connector' + }); + + var extensionElements = moddle.create('bpmn:ExtensionElements', { values: [ connector ] }); + + var serviceTask = moddle.create('bpmn:UserTask', { + extensionElements: extensionElements + }); + + var userTask = helper.clone(serviceTask, moddle.create('bpmn:UserTask'), [ + 'bpmn:extensionElements' + ]); + + var extElem = userTask.extensionElements; + + // then + expect(extElem.values).to.be.empty; + })); + }); });