blob: 782da3957e721c63e27f646aff9f6e49b46065e0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {Issue} from './issue.model';
import {Dsl, DslParser} from './dsl.model';
const MEMBERSPEC_REGEX = /^(\w+\.)*[mM]ember[sS]pec$/;
const FIRST_MEMBERSPEC_REGEX = /^(\w+\.)*first[mM]ember[sS]pec$/;
// TODO ideally we'd just look at type EntitySpec, not key name, but for now look at keyname, anything ending memberSpec
const ANY_MEMBERSPEC_REGEX = /^(\w+\.)*(\w*)[mM]ember[sS]pec$/;
const RESERVED_KEY_REGEX = /(^children$|^services$|^locations?$|^brooklyn\.config$|^brooklyn\.parameters$|^brooklyn\.enrichers$|^brooklyn\.policies$)/;
const FIELD = {
SERVICES: 'services', CHILDREN: 'brooklyn.children', CONFIG: 'brooklyn.config', PARAMETERS: 'brooklyn.parameters', LOCATION: 'location',
POLICIES: 'brooklyn.policies', ENRICHERS: 'brooklyn.enrichers', TYPE: 'type', NAME: 'name', ID: 'id',
// This field is not part of the Brooklyn blueprint spec but used to store information about the composer, e.g. X,Y coordinates, virtual items, etc
COMPOSER_META: 'brooklyn.composer.metadata'
};
const UNSUPPORTED_CATALOG_FIELDS = ['brooklyn.catalog', 'items', 'item'];
const UNSUPPORTED_FIELDS = {
locations: 'Multi-locations is not supported in the blueprint composer. Please use [location] instead'
};
export const EntityFamily = {
ENTITY: {id: 'ENTITY', displayName: 'Entity', superType: 'org.apache.brooklyn.api.entity.Entity'},
LOCATION: {id: 'LOCATION', displayName: 'Location', superType: 'org.apache.brooklyn.api.location'},
POLICY: {id: 'POLICY', displayName: 'Policy', superType: 'org.apache.brooklyn.api.policy.Policy'},
ENRICHER: {id: 'ENRICHER', displayName: 'Enricher', superType: 'org.apache.brooklyn.api.sensor.Enricher'},
SPEC: {id: 'SPEC', displayName: 'Spec', superType: 'org.apache.brooklyn.api.entity.EntitySpec'}
};
export const PREDICATE_MEMBERSPEC = (config, entity)=>(config.name.match(MEMBERSPEC_REGEX));
export const PREDICATE_FIRST_MEMBERSPEC = (config, entity)=>(config.name.match(FIRST_MEMBERSPEC_REGEX));
const DSL = {ENTITY_SPEC: '$brooklyn:entitySpec'};
const ID = new WeakMap();
const PARENT = new WeakMap();
const METADATA = new WeakMap();
const CONFIG = new WeakMap();
const PARAMETERS = new WeakMap();
const CHILDREN = new WeakMap();
const LOCATIONS = new WeakMap();
const POLICIES = new WeakMap();
const ENRICHERS = new WeakMap();
const MISC_DATA = new WeakMap();
/**
*
* @param {string} value
* @returns {boolean}
*/
const NOT_EMPTY = function (value) {
return (typeof value !== 'undefined' && value !== null && value.length > 0);
};
export class Entity {
constructor() {
ID.set(this, Math.random().toString(36).slice(2));
CONFIG.set(this, new Map());
PARAMETERS.set(this, []);
METADATA.set(this, new Map());
ENRICHERS.set(this, new Map());
POLICIES.set(this, new Map());
CHILDREN.set(this, []);
MISC_DATA.set(this, new Map());
MISC_DATA.get(this).set('issues', []);
this.family = EntityFamily.ENTITY.id;
this.touch();
}
/**
* The internal entity id
* @returns {string}
*/
get _id() {
return ID.get(this);
}
/**
* The external entity if set
* @returns {string}
*/
get id() {
return METADATA.get(this).get(FIELD.ID) || null;
}
/**
* Set the external id (NOTE:: This does not effect the value of the internal id `_id`)
* @param {string} id
*/
set id(id) {
METADATA.get(this).set(FIELD.ID, id);
this.touch();
}
/**
* @returns {boolean}
*/
hasId() {
return METADATA.get(this).has(FIELD.ID) && NOT_EMPTY(METADATA.get(this).get(FIELD.ID));
}
/**
*
*/
touch() {
//// uncomment the line below to include a summary to aid with debugging
//// (otherwise log just shows the property lastUpdated, until you expand)
// this.summary = (this.type || "unset") + (this.id ? " "+this.id : "");
this.lastUpdated = new Date().getTime();
if (this.hasParent()) {
this.parent.touch();
}
}
/**
* Has type been set
* @returns {boolean}
*/
hasType() {
return METADATA.get(this).has(FIELD.TYPE) && NOT_EMPTY(METADATA.get(this).get(FIELD.TYPE));
}
/**
* Get {Entity} type
* @returns {string}
*/
get type() {
return METADATA.get(this).get(FIELD.TYPE);
}
/**
* Set {Entity} type
* @param {string} type
*/
set type(type) {
if (NOT_EMPTY(type)) {
METADATA.get(this).set(FIELD.TYPE, type);
this.touch();
}
}
/**
* Get {Entity} type version
* @returns {string}
*/
get version() {
return MISC_DATA.get(this).get('version');
}
/**
* Set {Entity} type version
* @param {string} version
*/
set version(version) {
MISC_DATA.get(this).set('version', version);
}
/**
* @returns {boolean}
*/
hasVersion() {
return MISC_DATA.get(this).has('version');
}
/**
* Has name been set
* @returns {boolean}
*/
hasName() {
return METADATA.get(this).has(FIELD.NAME) && NOT_EMPTY(METADATA.get(this).get(FIELD.NAME));
}
/**
* Get {Entity} name
* @returns {string}
*/
get name() {
return METADATA.get(this).get(FIELD.NAME);
}
/**
* Set {Entity} name
* @param {string} name
*/
set name(name) {
METADATA.get(this).set(FIELD.NAME, name);
this.touch();
}
/**
* Get {Entity} family
* @returns {string}
*/
get family() {
return MISC_DATA.get(this).get('family');
}
/**
* Set {Entity} family
* @param {string} familyId
*/
set family(familyId) {
switch (familyId) {
case EntityFamily.ENRICHER.id:
MISC_DATA.get(this).set('family', EntityFamily.ENRICHER);
break;
case EntityFamily.POLICY.id:
MISC_DATA.get(this).set('family', EntityFamily.POLICY);
break;
case EntityFamily.SPEC.id:
MISC_DATA.get(this).set('family', EntityFamily.SPEC);
break;
case EntityFamily.ENTITY.id:
default:
MISC_DATA.get(this).set('family', EntityFamily.ENTITY);
}
}
/**
* Has {Entity} icon been set
* @returns {boolean}
*/
hasIcon() {
return MISC_DATA.get(this).has('icon') && NOT_EMPTY(MISC_DATA.get(this).get('icon'));
}
/**
* Get {Entity} icon
* @returns {string}
*/
get icon() {
return MISC_DATA.get(this).get('icon');
}
/**
* Set {Entity} type
* @param {string} icon
*/
set icon(icon) {
if (NOT_EMPTY(icon)) {
MISC_DATA.get(this).set('icon', icon);
this.touch();
}
}
/**
* Get {Entity} location
* @returns {string}
*/
get location() {
return LOCATIONS.get(this);
}
/**
* Set {Entity} location
* @param {string} location
*/
set location(location) {
LOCATIONS.set(this, location);
this.miscData.delete('locationRemoved');
this.touch();
}
/**
* Remove {Entity} location
* @returns {string}
*/
removeLocation() {
LOCATIONS.delete(this);
this.miscData.delete('locationName');
this.miscData.delete('locationIcon');
// this field provides a way for consumers to detect if the location was explicitly removed;
// this can be useful to prevent default locations from being applied
this.miscData.set('locationRemoved', true);
this.touch();
}
/**
* Get {Entity} parent
* @returns {Entity}
*/
get parent() {
return PARENT.get(this);
}
/**
* Set {Entity} parent
* @param {Entity} parent
*/
set parent(parent) {
if (parent instanceof Entity) {
if (PARENT.get(this) !== parent) {
PARENT.set(this, parent);
this.touch();
}
} else {
throw new Error('Cannot add parent ... parent must be of type Entity');
}
}
get children() {
return CHILDREN.get(this);
}
get childrenAsMap() {
return CHILDREN.get(this).reduce((map, child) => {
map.set(child._id, child);
return map;
}, new Map());
}
get config() {
return CONFIG.get(this);
}
get parameters() {
return PARAMETERS.get(this);
}
get metadata() {
return METADATA.get(this);
}
get issues() {
return MISC_DATA.get(this).get('issues');
}
/**
* Add child {Entity}
* @param {Entity} child
* @returns {Entity}
*/
addChild(child) {
if (child instanceof Entity) {
child.parent = this;
CHILDREN.get(this).push(child);
this.touch();
return this;
} else {
throw new Error('Cannot add child ... child must be of type Entity');
}
}
/**
* Insert child {Entity} at a given position
* @param {Entity} child
* @param {number} index, zero-based
* @return {Entity}
*/
insertChild(child, index) {
if (child instanceof Entity) {
if (index < 0 || index > CHILDREN.get(this).length) {
throw new Error('Cannot insert child ... invalid index value ' + index);
}
child.parent = this;
CHILDREN.get(this).splice(index, 0, child);
this.touch();
return this;
} else {
throw new Error('Cannot insert child ... child must be of type Entity');
}
}
addEnricher(enricher) {
if (enricher instanceof Entity) {
enricher.parent = this;
enricher.family = EntityFamily.ENRICHER.id;
ENRICHERS.get(this).set(enricher._id, enricher);
this.touch();
return this;
} else {
throw new Error('Cannot add enricher ... enricher must be of type Entity');
}
}
addNewEnricher() {
let newEnricher = new Entity();
this.addEnricher(newEnricher);
return newEnricher;
}
removeEnricher(id) {
ENRICHERS.get(this).delete(id);
this.touch();
return this;
}
addPolicy(policy) {
if (policy instanceof Entity) {
policy.parent = this;
policy.family = EntityFamily.POLICY.id;
POLICIES.get(this).set(policy._id, policy);
this.touch();
return this;
} else {
throw new Error('Cannot add policy ... policy must be of type policy');
}
}
addNewPolicy() {
let newPolicy = new Entity();
this.addPolicy(newPolicy);
return newPolicy;
}
removePolicy(id) {
POLICIES.get(this).delete(id);
this.touch();
return this;
}
/**
* Remove child
* @param {string} id
* @returns {Entity}
*/
removeChild(id) {
if (this.hasChildren()) {
let childIndex = CHILDREN.get(this)
.filter(e => e._id === id)
.map(e => CHILDREN.get(this).indexOf(e));
if (childIndex.length > 0) {
let removed = CHILDREN.get(this).splice(childIndex[0], 1);
PARENT.delete(removed[0]);
this.touch();
}
}
return this;
}
/**
* Has {Entity} got another Entity as an ancestor
* @param entity
* @return {boolean} <code>true</code> if the given entity is an ancestor of this
*/
hasAncestor(entity) {
if (this === entity) {
return true;
}
else if (this.hasParent()) {
return this.parent.hasAncestor(entity);
}
else {
return false;
}
}
/**
* Has {Entity} got a parent
* @returns {boolean}
*/
hasParent() {
return PARENT.has(this);
}
/**
* Has {Entity} got children
* @returns {boolean}
*/
hasChildren() {
return CHILDREN.get(this).length > 0;
}
/**
* Has {Entity} got config
* @returns {boolean}
*/
hasConfig() {
return CONFIG.get(this).size > 0;
}
/**
* Has {Entity} got parameters
* @returns {boolean}
*/
hasParameters() {
return PARAMETERS.get(this).length > 0;
}
/**
* Has {Entity} got a location
* @returns {boolean}
*/
hasLocation() {
return LOCATIONS.has(this);
}
/**
* Has {Entity} got policies
* @returns {boolean}
*/
hasPolicies() {
return POLICIES.get(this).size > 0;
}
/**
* Has {Entity} got enrichers
* @returns {boolean}
*/
hasEnrichers() {
return ENRICHERS.get(this).size > 0;
}
//NEW
get metadata() {
return METADATA.get(this);
}
get enrichers() {
return ENRICHERS.get(this);
}
getEnrichersAsArray() {
return Array.from(ENRICHERS.get(this).values());
}
get policies() {
return POLICIES.get(this);
}
getPoliciesAsArray() {
return Array.from(POLICIES.get(this).values());
}
get miscData() {
return MISC_DATA.get(this);
}
equals(value) {
if (value && value instanceof Entity) {
try {
return (this.getData(true) === value.getData(true));
} catch (err) {
}
}
return false;
}
toString() {
return 'Entity :: id = [' + this._id + ']' + (this.hasType() ? ' type = [' + this.type + ']' : '');
}
}
Entity.prototype.setEntityFromJson = setEntityFromJson;
Entity.prototype.setChildrenFromJson = setChildrenFromJson;
Entity.prototype.getConfigAsJson = getConfigAsJson;
Entity.prototype.setConfigFromJson = setConfigFromJson;
Entity.prototype.getParametersAsArray = getParametersAsArray;
Entity.prototype.setParametersFromJson = setParametersFromJson;
Entity.prototype.getMetadataAsJson = getMetadataAsJson;
Entity.prototype.setMetadataFromJson = setMetadataFromJson;
Entity.prototype.setEnrichersFromJson = setEnrichersFromJson;
Entity.prototype.setPoliciesFromJson = setPoliciesFromJson;
Entity.prototype.getData = getData;
Entity.prototype.addConfig = addConfig;
Entity.prototype.addParameter = addParameter;
Entity.prototype.addMetadata = addMetadata;
Entity.prototype.removeConfig = removeConfig;
Entity.prototype.removeParameter = removeParameter;
Entity.prototype.removeMetadata = removeMetadata;
Entity.prototype.isCluster = isCluster;
Entity.prototype.isMemberSpec = isMemberSpec;
Entity.prototype.setClusterMemberspecEntity = setClusterMemberspecEntity;
Entity.prototype.getClusterMemberspecEntity = getClusterMemberspecEntity;
Entity.prototype.getClusterMemberspecEntities = getClusterMemberspecEntities;
Entity.prototype.getInheritedLocation = getInheritedLocation;
Entity.prototype.hasInheritedLocation = hasInheritedLocation;
Entity.prototype.addIssue = addIssue;
Entity.prototype.hasIssues = hasIssues;
Entity.prototype.clearIssues = clearIssues;
Entity.prototype.resetIssues = resetIssues;
Entity.prototype.delete = deleteEntity;
Entity.prototype.reset = resetEntity;
/**
* Add an entry to brooklyn.config
* @param {string} key
* @param {*} value
* @returns {Entity}
*/
function addConfig(key, value) {
if (ANY_MEMBERSPEC_REGEX.test(key) && value.hasOwnProperty(DSL.ENTITY_SPEC)) {
if (value[DSL.ENTITY_SPEC] instanceof Entity) {
value[DSL.ENTITY_SPEC].family = EntityFamily.SPEC.id;
value[DSL.ENTITY_SPEC].parent = this;
CONFIG.get(this).set(key, value);
} else {
var entity = new Entity().setEntityFromJson(value[DSL.ENTITY_SPEC]);
entity.family = EntityFamily.SPEC.id;
entity.parent = this;
CONFIG.get(this).set(key, {'$brooklyn:entitySpec': entity});
}
this.touch();
return this;
} else {
CONFIG.get(this).set(key, value);
this.touch();
return this;
}
}
function addParameter(param) {
PARAMETERS.get(this).push(param);
this.touch();
return this;
}
function addMetadata(key, value) {
if (!RESERVED_KEY_REGEX.test(key)) {
METADATA.get(this).set(key, value);
this.touch();
} else {
// TODO inject $log service
console.log("Cannot add metadata for reserved word", key, value);
}
return this;
}
/**
* Remove an entry from brooklyn.config
* @param {string} key
* @returns {Entity}
*/
function removeConfig(key) {
CONFIG.get(this).delete(key);
this.touch();
return this;
}
/**
* Remove an entry from brooklyn.parameters
* @param {string} name
* @returns {Entity}
*/
function removeParameter(name) {
if (this.hasParameters()) {
let paramIndex = PARAMETERS.get(this).findIndex(e => e.name === name);
if (paramIndex != -1) {
PARAMETERS.get(this).splice(paramIndex, 1);
this.touch();
}
}
return this;
}
/**
* Update an entry in brooklyn.parameters
* @param {string} name
* @param {object} definition
* @returns {Entity}
*/
function updateParameter(name, definition) {
if (this.hasParameters()) {
let paramIndex = PARAMETERS.get(this).findIndex(e => e.name === name);
if (paramIndex != -1) {
PARAMETERS.get(this)[paramIndex] = definition;
this.touch();
}
}
return this;
}
/**
* Remove an entry from the entity metadata
* @param {string} key
* @returns {Entity}
*/
function removeMetadata(key) {
METADATA.get(this).delete(key);
this.touch();
return this;
}
/**
*
* @returns {boolean}
*/
function isCluster() {
if (!MISC_DATA.get(this).has('traits')) {
return false;
}
let traits = MISC_DATA.get(this).get('traits');
return traits && traits.filter((trait)=> {
return ['org.apache.brooklyn.entity.group.Cluster',
'org.apache.brooklyn.entity.group.Fabric']
.indexOf(trait) !== -1
}).length > 0;
}
/**
*
* @returns {boolean}
*/
function isMemberSpec() {
return this.parent && this.parent.isCluster() && this.parent.getClusterMemberspecEntity() === this;
}
export function baseType(s) {
if (s && s.indexOf("<")>=0) {
s = s.substring(0, s.indexOf("<"));
}
return s;
}
/**
* Returns a map of <configkey> => Entity of all spec {Entity} defined in the configuration
* @returns {*}
*/
function getClusterMemberspecEntities() {
if (!MISC_DATA.get(this).has('config')) {
return {};
}
return MISC_DATA.get(this).get('config')
.filter((config)=>(baseType(config.type) === EntityFamily.SPEC.superType))
.reduce((acc, config)=> {
if (CONFIG.get(this).has(config.name)) {
acc[config.name] = CONFIG.get(this).get(config.name)[DSL.ENTITY_SPEC];
}
return acc;
}, {});
}
/**
* Returns the first memberspec that matches the given predicate
*
* @param predicate A predicate function to filter the results. it takes the config key definition and the entity as parameters
* @returns {Entity}
*/
function getClusterMemberspecEntity(predicate = ()=>(true)) {
if (!MISC_DATA.get(this).has('config')) {
return undefined;
}
return MISC_DATA.get(this).get('config')
.filter((config)=>(baseType(config.type) === EntityFamily.SPEC.superType))
.reduce((acc, config)=> {
if (CONFIG.get(this).has(config.name)) {
let entityV = CONFIG.get(this).get(config.name)[DSL.ENTITY_SPEC];
if (entityV && predicate(config, entityV)) {
return entityV;
}
}
return acc;
}, undefined);
}
function setClusterMemberspecEntity(key, entity) {
if (!MISC_DATA.get(this).has('config')) {
return this;
}
let definition = MISC_DATA.get(this).get('config')
.filter((config)=>(baseType(config.type) === EntityFamily.SPEC.superType && config.name === key));
if (definition.length !== 1) {
return this;
}
if (entity instanceof Entity) {
let value = {};
value[DSL.ENTITY_SPEC] = entity;
this.addConfig(key, value);
this.touch();
}
return this;
}
/**
* Retrieve the {Entity} as JSON
* @param {boolean} includeChildren
* @returns {{}}
*/
function getData(includeChildren = true) {
if (!ID.has(this)) { // Entity has already been garbage collected
return {};
}
var result = this.getMetadataAsJson();
if (this.hasConfig()) {
result[FIELD.CONFIG] = this.getConfigAsJson();
}
if (this.hasParameters()) {
result[FIELD.PARAMETERS] = this.getParametersAsArray();
}
if (this.hasLocation()) {
result.location = LOCATIONS.get(this);
}
if (this.hasChildren() && includeChildren) {
var children = [];
for (let child of CHILDREN.get(this).values()) {
children.push(child.getData());
}
if (this.hasParent()) {
result[FIELD.CHILDREN] = children;
} else {
result[FIELD.SERVICES] = children;
}
}
if (this.hasPolicies()) {
var policies = [];
for (let policy of POLICIES.get(this).values()) {
policies.push(policy.getData());
}
result[FIELD.POLICIES] = policies;
}
if (this.hasEnrichers()) {
var enrichers = [];
for (let enricher of ENRICHERS.get(this).values()) {
enrichers.push(enricher.getData());
}
result[FIELD.ENRICHERS] = enrichers;
}
return deepMerge(result, this.miscData.get('virtual'));
}
/**
* Retrieve the inherited location coming from any parent {Entity}. Returns null if any.
* @returns {string}
*/
function getInheritedLocation() {
if (this.hasParent()) {
if (this.parent.hasLocation()) {
return this.parent.miscData.get('locationName') || this.parent.location;
} else {
return this.parent.getInheritedLocation();
}
}
return null;
}
/**
* Returns true if the current {Entity} has an inherited location coming from any parent {Entity}.
* @returns {boolean}
*/
function hasInheritedLocation() {
return this.getInheritedLocation() !== null;
}
function addIssue(issue) {
if (issue instanceof Issue) {
this.issues.push(issue);
this.touch();
}
return this;
}
function hasIssues() {
return this.issues.length > 0;
}
function clearIssues(predicate) {
if (this.hasIssues()) {
if (predicate && predicate instanceof Object) {
MISC_DATA.get(this).set('issues', this.issues.filter(issue => {
let condition = true;
Object.keys(predicate).forEach(key => {
if (Object.getOwnPropertyDescriptor(Issue.prototype, key)) {
condition &= predicate[key] === issue[key];
}
});
return !condition;
}));
} else {
this.resetIssues();
}
this.touch();
}
return this;
}
function resetIssues() {
MISC_DATA.get(this).set('issues', []);
this.touch();
return this;
}
/**
* Delete this entity
*/
function deleteEntity() {
if (this.hasParent()) {
this.parent.removeChild(this._id);
} else {
this.reset();
}
}
/**
* Reset this entity
*/
function resetEntity() {
ID.set(this, Math.random().toString(36).slice(2));
this.removeLocation();
CONFIG.set(this, new Map());
PARAMETERS.set(this, []);
METADATA.set(this, new Map());
ENRICHERS.set(this, new Map());
POLICIES.set(this, new Map());
CHILDREN.set(this, []);
MISC_DATA.set(this, new Map());
MISC_DATA.get(this).set('issues', []);
this.family = EntityFamily.ENTITY.id;
this.touch();
}
function isDslish(x) {
if (typeof x === 'string' && x.startsWith('$brooklyn:')) return true;
}
/**
* Set entity from JSON object
* @param {{}} incomingModel
* @param {boolean} setChildren
* @returns {Entity}
*/
function setEntityFromJson(incomingModel, setChildren = true) {
// ideally we'd be able to detect the type of `incomingModel`;
// imagine we had `DslExpression` as a type and a `DslEntitySpecExpression` as a subclass of `DslExpression`, then:
// * this code should throw an error if it's not-null, not undefined, but not a `DslExpression`.
// * the UI should then render it differently whether it is a `DslEntitySpecExpression` or not.
// but for now we have the isDslish hack, and the UI renders it based on a REPLACED_DSL_ENTITYSPEC marker
if (incomingModel && incomingModel.constructor.name !== 'Object') {
if (isDslish(incomingModel)) {
// no error
} else {
throw new TypeError('Entity cannot be set from [' + incomingModel.constructor.name + '] ... please supply an [Object]');
}
}
METADATA.get(this).clear();
return Object.keys(incomingModel).reduce((self, key)=> {
if (UNSUPPORTED_CATALOG_FIELDS.indexOf(key) !== -1) {
throw new Error('Catalog format not supported ... unsupported field [' + key + ']');
}
if (Object.keys(UNSUPPORTED_FIELDS).indexOf(key) !== -1) {
throw new Error(`Field [${key}] not supported ... ${UNSUPPORTED_FIELDS[key]}`);
}
switch (key) {
case FIELD.CHILDREN:
case FIELD.SERVICES:
if (setChildren) {
self.setChildrenFromJson(incomingModel[key]);
}
break;
case FIELD.CONFIG:
self.setConfigFromJson(incomingModel[key]);
break;
case FIELD.PARAMETERS:
self.setParametersFromJson(incomingModel[key]);
break;
case FIELD.ENRICHERS:
self.setEnrichersFromJson(incomingModel[key]);
break;
case FIELD.POLICIES:
self.setPoliciesFromJson(incomingModel[key]);
break;
case FIELD.LOCATION:
this.location = incomingModel[key];
break;
case FIELD.TYPE:
let parsedType = incomingModel[key].split(':');
self.addMetadata(key, parsedType[0]);
self.miscData.delete('version');
if (parsedType.length > 1) {
self.miscData.set('version', parsedType[1]);
}
break;
// This field is use to pass back information about a virtual item. A virtual item is an item that is not present
// within the Brooklyn Catalog but translate to a real blueprint. As the composer is bidirectional, it requires
// at least the virtual type that will replace the real type within the internal model. The composer will then
// use its standard routines to get back the rest of the information. This needs to be used in concert with a
// customised PaletteApiProvider implementation to get back the information about the virtual item.
case FIELD.COMPOSER_META:
let composerMetadata = incomingModel[key];
if (composerMetadata.hasOwnProperty('virtualType')) {
self.addMetadata(FIELD.TYPE, composerMetadata.virtualType);
}
break;
default:
self.addMetadata(key, incomingModel[key]);
}
return self;
}, this);
}
/**
* Set {Entity} childen from JSON {Array}
* @param {Array} incomingModel
*/
function setChildrenFromJson(incomingModel) {
if (!Array.isArray(incomingModel)) {
throw new Error('Model parse error ... cannot add children as it must be an array')
}
let children = [];
incomingModel.reduce((self, child)=> {
let childEntity = new Entity();
childEntity.setEntityFromJson(child);
childEntity.parent = self;
children.push(childEntity);
return this;
}, this);
CHILDREN.set(this, children);
this.touch();
}
/**
* Set brooklyn.config from JSON
* @param {{}} incomingModel
*/
function setConfigFromJson(incomingModel) {
CONFIG.get(this).clear();
let self = this;
Object.keys(incomingModel).forEach((key)=>(self.addConfig(key, incomingModel[key])));
this.touch();
}
/**
* Set brooklyn.parameters from JSON {Array}
* @param {Array} incomingModel
*/
function setParametersFromJson(incomingModel) {
if (!Array.isArray(incomingModel)) {
throw new Error('Model parse error ... cannot add parameters as it must be an array')
}
PARAMETERS.set(this, []);
let self = this;
incomingModel.map((param)=> {
self.addParameter(param);
});
this.touch();
}
function setMetadataFromJson(incomingModel) {
METADATA.get(this).clear();
let self = this;
Object.keys(incomingModel).forEach((key)=> (self.addMetadata(key, incomingModel[key])));
this.touch();
}
/**
* Set {Entity} enrichers from JSON {Array}
* @param {Array} incomingModel
*/
function setEnrichersFromJson(incomingModel) {
if (!Array.isArray(incomingModel)) {
throw new Error('Model parse error ... cannot add enrichers as it must be an array')
}
ENRICHERS.get(this).clear();
let self = this;
incomingModel.map((enricher)=> {
let newEnricher = new Entity();
newEnricher.setEntityFromJson(enricher);
newEnricher.parent = self;
self.addEnricher(newEnricher);
});
this.touch();
}
/**
* Set {Entity} policies from JSON {Array}
* @param {Array} incomingModel
*/
function setPoliciesFromJson(incomingModel) {
if (!Array.isArray(incomingModel)) {
throw new Error('Model parse error ... cannot add policies as it must be an array')
}
POLICIES.get(this).clear();
let self = this;
incomingModel.map((policy)=> {
let newPolicy = new Entity();
newPolicy.setEntityFromJson(policy);
newPolicy.parent = self;
self.addPolicy(newPolicy);
});
this.touch();
}
function getMetadataAsJson() {
let metadata = cleanForJson(METADATA.get(this), -1);
if (metadata.hasOwnProperty(FIELD.TYPE) && this.hasVersion()) {
metadata[FIELD.TYPE] += ':' + this.version;
}
return metadata;
}
function getConfigAsJson() {
return cleanForJson(CONFIG.get(this), -1);
}
function getParametersAsArray() {
return PARAMETERS.get(this);
}
/* "cleaning" here means: Dsl objects are toStringed, to the given depth (or infinite if depth<0);
* and entries in Map that are memberspec are unwrapped.
* previously we also stringified maps/lists but that seemed pointless, and it was lossy and buggy.
*/
function cleanForJson(item, depth) {
if (depth==0) {
return item;
}
if (item instanceof Dsl) {
// return the string value so that the json is accurate
// (otherwise it goes through keys below, which is wrong)
return item.toString();
}
if (item instanceof Map) {
var result = {};
for (var [key, value] of item) {
if (ANY_MEMBERSPEC_REGEX.test(key) && value.hasOwnProperty(DSL.ENTITY_SPEC)) {
var _jsonVal = {};
_jsonVal[DSL.ENTITY_SPEC] = value[DSL.ENTITY_SPEC].getData();
result[key] = _jsonVal;
} else {
result[key] = cleanForJson(value, depth-1);
}
}
return result;
}
if (item instanceof Array) {
return item.map(item2 => cleanForJson(item2, depth-1));
}
if (item instanceof Object) {
return Object.keys(item).reduce((o, key) => {
o[key] = cleanForJson(item[key], depth-1);
return o;
}, {});
}
return item;
}
/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
*
* @param {...object} objects - Objects to merge
* @returns {object} New object with merged key/values
*/
function deepMerge(...objects) {
const isObject = obj => obj && typeof obj === 'object';
if (objects.length === 1) {
return objects[0];
}
return objects.reduce((acc, obj) => {
if (obj === null || typeof obj === 'undefined') {
return acc;
}
Object.keys(obj).forEach(key => {
const currenValue = acc[key];
const valueToMerge = obj[key];
if (Array.isArray(currenValue) && Array.isArray(valueToMerge)) {
acc[key] = currenValue.concat(...valueToMerge);
}
else if (isObject(currenValue) && isObject(valueToMerge)) {
acc[key] = deepMerge(currenValue, valueToMerge);
}
else {
acc[key] = valueToMerge;
}
});
return acc;
}, {});
}
export class EntityError extends Error {
constructor(message, options = {}) {
super(message);
this.name = 'EntityError';
this.message = message;
this.id = options.id || 'general-error';
this.data = options.data || null;
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error(message)).stack;
}
}
}