function BlueprintService()

in ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js [55:937]


function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService, quickLaunchOverrides, brBrandInfo) {
    const quickLaunchUtils = {}
    quickLaunchOverrides.configureQuickLaunch(quickLaunchUtils);

    let blueprint = new Entity();
    let entityRelationshipProviders = {};

    // Add relationships provider based on Entity.config
    addEntityRelationshipsProvider( 'config',{
        apply: (entity) => {
            let set = Array.from(entity.config.keys())
                .reduce((set, key)=> {
                    let config = entity.config.get(key);
                    if (config instanceof Dsl) {
                        config.relationships.forEach((entity) => {
                            if (entity !== null) {
                                set.add({entity: entity, name: key});
                            }
                        });
                    }
                    if (config instanceof Array) {
                        config
                            .filter(item => item instanceof Dsl)
                            .reduce((set, config) => {
                                config.relationships.forEach((entity) => {
                                    if (entity !== null) {
                                        set.add({entity: entity, name: key});
                                    }
                                });
                                return set;
                            }, set);
                    }
                    if (config instanceof Object) {
                        Object.keys(config)
                            .filter(objectKey => config[objectKey] instanceof Dsl)
                            .reduce((set, objectKey) => {
                                    config[objectKey].relationships.forEach((entity) => {
                                        if (entity !== null) {
                                            // name is the name of a complex relationship, and it consists of a property name, not its members. That is why here, name is set to 'key' not 'objectKey'.
                                            set.add({entity: entity, name: key});
                                        }
                                    });
                                return set;
                            }, set);
                    }
                    return set;
                }, new Set());

            return Array.from(set).map((relation) => {
                return {
                    source: entity,
                    target: relation.entity,
                    label: relation.name
                };
            });
        }
    });

    // Add relationships provider based on Entity spec
    addEntityRelationshipsProvider('spec', {
        apply: (entity) => {
            return Array.from(entity.config.values())
                .filter(config => config && config[DSL_ENTITY_SPEC] && config[DSL_ENTITY_SPEC] instanceof Entity)
                .map(config => config[DSL_ENTITY_SPEC])
                .reduce((relationships, spec) => relationships.concat(getRelationships(spec)), []);
        }
    });

    return {
        setFromJson: setBlueprintFromJson,
        setFromYaml: setBlueprintFromYaml,
        get: getBlueprint,
        getAsJson: getBlueprintAsJson,
        getAsYaml: getBlueprintAsYaml,
        reset: resetBlueprint,
        find: findEntity,
        findAny: findAnyEntity,
        getEntityMetadata: getEntityMetadata,
        entityHasMetadata: entityHasMetadata,
        refreshBlueprintMetadata: refreshBlueprintMetadata,
        refreshEntityMetadata: refreshEntityMetadata,
        refreshConfigConstraints: refreshConfigConstraints,
        refreshRelationships: refreshRelationships,
        refreshAllRelationships: refreshAllRelationships,
        refreshConfigInherited: refreshConfigInherited,
        addRelationshipsProvider: addEntityRelationshipsProvider,
        isReservedKey: isReservedKey,
        getIssues: getIssues,
        hasIssues: hasIssues,
        clearAllIssues: clearAllIssues,
        getAllIssues: getAllIssues,
        populateEntityFromApi: populateEntityFromApiSuccess,
        populateLocationFromApi: populateLocationFromApiSuccess,
        addConfigKeyDefinition: addConfigKeyDefinition,
        addParameterDefinition: addParameterDefinition,
        getRelationships: getRelationships,
        populateId: populateId,
    };

    function setBlueprintFromJson(jsonBlueprint) {
        if (jsonBlueprint) {
            blueprint.setEntityFromJson(jsonBlueprint);
            $log.debug(TAG + 'Blueprint set from JS', blueprint);
        } else {
            resetBlueprint();
        }
    }

    function setBlueprintFromYaml(yamlBlueprint, reset = false) {
        let newBlueprint = jsYaml.safeLoad(yamlBlueprint);
        if (reset) {
            resetBlueprint();
        }
        setBlueprintFromJson(newBlueprint);
        $log.debug(TAG + 'Blueprint set from YAML', blueprint);
        refreshBlueprintMetadata(); // needed to prevent graph child nodes from disappearing when the "Add to catalog" modal is opened
    }

    function getBlueprint() {
        return blueprint;
    }

    function getBlueprintAsJson() {
        return blueprint.getData();
    }

    function getBlueprintAsYaml() {
        try {
            let currentModel = blueprint.getData();
            return (Object.keys(currentModel).length === 0) ? '' : jsYaml.safeDump(currentModel, { lineWidth: 127 });
        } catch (err) {
            $log.error("Error converting brooklyn blueprint (rethrowing)", err);
            throw 'Could not convert the Brooklyn blueprint into YAML format';
        }
    }

    function resetBlueprint() {
        blueprint.reset();
    }

    function findEntity(id) {
        if (!id) {
            return null;
        }
        return lookup(blueprint, id);
    }


    function findAnyEntity(id) {
        if (!id) {
            return null;
        }
        return lookup(blueprint, id, true);
    }

    function getEntityMetadata(entity) {
        let metadata = new Map();
        if (entity instanceof Entity) {
            entity.metadata.forEach((value, key)=> {
                if (RESERVED_KEYS.indexOf(key) === -1) {
                    metadata.set(key, value);
                }
            });
        }
        return metadata;
    }

    function entityHasMetadata(entity) {
        return this.getEntityMetadata(entity).size > 0;
    }

    function isReservedKey(key) {
        return RESERVED_KEYS.indexOf(key) > -1;
    }

    function getIssues(entity = blueprint, nonRecursive) {
        let issues = [];

        if (entity.hasIssues()) {
            issues = issues.concat(entity.issues.map((issue) => {
                let newIssue = Issue.builder().message(issue.message).group(issue.group).ref(issue.ref).level(issue.level).build();
                newIssue.entity = entity;
                return newIssue;
            }));
        }

        entity.policies.forEach((policy) => {
            issues = issues.concat(getIssues(policy));
        });

        entity.enrichers.forEach((enricher) => {
            issues = issues.concat(getIssues(enricher));
        });

        if (!nonRecursive) {
            entity.children.forEach((child) => {
                issues = issues.concat(getIssues(child));
            });
        }

        return issues;
    }

    // typically followed by a call to refresh
    function clearAllIssues(entity = blueprint) {
        entity.resetIssues();
        entity.children.forEach(clearAllIssues);
    }

    function getAllIssues(entity = blueprint) {
        return collectAllIssues({}, entity);
    }

    function collectAllIssues(result, entity) {
        if (!result.entities) {
            result.entities = {};
            result.byEntity = {};
            result.count = 0;
            result.errors = { byEntity: {}, count: 0 }
            result.warnings = { byEntity: {}, count: 0 }
        }
        if (result.entities[entity._id]) {
            // already visited, some sort of reference; ignore
        } else {
            result.entities[entity._id] = entity;

            let issues = getIssues(entity, true);
            if (issues.length) {
                result.byEntity[entity._id] = issues;
                result.count += issues.length;

                let errors = issues.filter(i => i.level.id == 'error');
                if (errors.length) {
                    result.errors.byEntity[entity._id] = errors;
                    result.errors.count += errors.length;
                }

                let warnings = issues.filter(i => i.level.id != 'error');
                if (warnings.length) {
                    result.warnings.byEntity[entity._id] = warnings;
                    result.warnings.count += warnings.length;
                }
            }

            entity.children.forEach((child)=> collectAllIssues(result, child));
        }
        return result;
    }

    function hasIssues() {
        return this.getIssues().length > 0;
    }

    function lookup(entity, id, any = false) {
        if (entity._id === id) {
            return entity;
        }
        if (entity.childrenAsMap.has(id)) {
            return entity.childrenAsMap.get(id);
        }
        if (entity.policies.has(id)) {
            return entity.policies.get(id);
        }
        if (entity.enrichers.has(id)) {
            return entity.enrichers.get(id);
        }
        let memberSpecs = Object.values(entity.getClusterMemberspecEntities()).filter((memberSpec)=>(memberSpec._id === id));
        if (memberSpecs.length === 1) {
            return memberSpecs[0];
        }
        for (let child of entity.childrenAsMap.values()) {
            let ret = lookup(child, id, any);
            if (ret !== null) {
                return ret;
            }
        }
        return null;
    }

    function refreshBlueprintMetadata(entity = blueprint, family = 'ENTITY') {
        // TODO ideally we'd have a cache for all types
        return refreshEntityMetadata(entity, family).then(()=> {
            return $q.all(entity.children.reduce((result, child) => {
                result.push(refreshBlueprintMetadata(child));
                return result;
            }, []));
        }).then(() => {
            return entity;
        });
    }

    function refreshEntityMetadata(entity, family) {
        entity.miscData.set('loading', true);
        return $q.all([refreshTypeMetadata(entity, family), refreshLocationMetadata(entity)]).then(()=> {
            entity.miscData.set('loading', false);
            return refreshRelationships(entity);
        }).then(() => {
            entity.clearIssues({group: 'config'});
            return $q.all([
                refreshConfigConstraints(entity),
                refreshConfigMemberspecsMetadata(entity),
                refreshPoliciesMetadata(entity),
                refreshEnrichersMetadata(entity),
                refreshConfigInherited(entity)
            ]);
        }).then(()=> {
            return entity;
        });
    }

    function refreshTypeMetadata(entity, family) {
        let deferred = $q.defer();
        if (entity.hasType()) {
            entity.family = family.id;

            let promise = entity.miscData.has('bundle')
                ? paletteApi.getBundleType(entity.miscData.get('bundle').symbolicName, entity.miscData.get('bundle').version, entity.type, entity.version, entity.config)
                : paletteApi.getType(entity.type, entity.version, entity.config);

            promise.then((data)=> {
                quickLaunchUtils.getAsCampPlan(data.plan).then(({parsedPlan})=>{
                    deferred.resolve(populateEntityFromApiSuccess(entity, data, parsedPlan));
                }).catch(function (error) {
                    console.debug("No parsed plan available", error, data);
                    deferred.resolve(populateEntityFromApiSuccess(entity, data));
                })
            }).catch(function (error) {
                deferred.resolve(populateEntityFromApiError(entity, error));
            });
        } else {
            if (entity.parent) {
                entity.clearIssues({group: 'type'}).addIssue(Issue.builder().group('type').message('Entity needs a type').level(ISSUE_LEVEL.WARN).build());
            }
            entity.miscData.set('sensors', []);
            entity.miscData.set('traits', []);

            entity.clearIssues({group: 'config'});
            entity.miscData.set('config', []);
            entity.miscData.set('configMap', {});
            entity.miscData.set('parameters', []);
            entity.miscData.set('parametersMap', {});

            addUnlistedConfigKeysDefinitions(entity);
            addUnlistedParameterDefinitions(entity);

            deferred.resolve(entity);
        }

        return deferred.promise;
    }

    function locationType(location) {
        if (!location || typeof location === 'string') return location;
        if (typeof location === 'object' && Object.keys(location).length==1) return Object.keys(location)[0];
        return null;
    }

    function refreshLocationMetadata(entity) {
        let deferred = $q.defer();

        if (entity.hasLocation()) {
            let type = locationType(entity.location);
            if (type && type.startsWith) {
                if (type.startsWith("jclouds:")) {
                    // types eg jclouds:aws-ec2 are low-level, not in the catalog
                    deferred.resolve(populateLocationFromApiSuccess(entity, { yamlHere: entity.location }));
                } else {
                    paletteApi.getLocation(locationType(entity.location)).then((location) => {
                        let loc = Object.assign({}, location.catalog || location, {yamlHere: entity.location});
                        deferred.resolve(populateLocationFromApiSuccess(entity, loc));
                    }).catch(function () {
                        deferred.resolve(populateLocationFromApiError(entity));
                    });
                }
            } else {
                deferred.resolve(entity);
            }
        } else {
            deferred.resolve(entity);
        }

        return deferred.promise;
    }

    function refreshConfigConstraints(entity) {
        function checkSensitiveFields(config) {
            if (isSensitiveFieldPlaintextValueBlocked() && isSensitiveFieldName(config.name)) {
                let v = entity.config.get(config.name);
                if (!v) return;
                let t = typeof v;
                if (t === 'object') return;
                let invalid = false;
                if (t === 'string') {
                    if (t.length) {
                        if (t.startsWith("$brooklyn:")) {
                            invalid = false;
                        } else {
                            invalid = true;
                        }
                    }
                } else if (t === 'number') {
                    invalid = true;
                }
                if (invalid) {
                    let message = `Plaintext values are not permitted for <samp>${config.name}</samp>. <br/>Use DSL with externalized configuration.`;
                    entity.addIssue(Issue.builder().group('config').ref(config.name).message($sce.trustAsHtml(message)).build());
                }
            }
        }
        function checkConstraints(config) {
            for (let constraintO of config.constraints) {
                let message = null;
                let key = null, args = null;
                if (constraintO instanceof String || typeof constraintO=='string') {
                    key = constraintO;
                } else if (Object.keys(constraintO).length==1) {
                    key = Object.keys(constraintO)[0];
                    args = constraintO[key];
                } else {
                    $log.warn("Unknown constraint object", typeof constraintO, constraintO, config);
                    key = constraintO;
                }
                let valExplicit = (k) => entity.config.get(k || config.name);
                let isSet = (k) => entity.config.has(k || config.name) && angular.isDefined(valExplicit(k));
                let val = (k) => isSet(k) ? entity.config.get(k || config.name) : config.defaultValue;
                let isAnySet = (k) => {
                    if (!k || !Array.isArray(k)) return false;
                    return k.some(isSet);
                }
                const hasDefault = (typeof config.defaultValue) !== 'undefined';

                switch (key) {
                    case 'Predicates.notNull()':
                    case 'Predicates.notNull':
                        if ((!isSet() && !hasDefault) || val()===null) {
                            message = `<samp>${config.name}</samp> is required`;
                        }
                        break;
                    case 'required':
                        if ((!isSet() && !hasDefault) || val()===null || val()==='') {
                            message = `<samp>${config.name}</samp> is required`;
                        }
                        break;
                    case 'regex':
                        let matchFullLine = '^' + args + '$';
                        if (isSet() && !(val() instanceof Dsl) && !(new RegExp(matchFullLine).test(val()))) {
                            message = `<samp>${config.name}</samp> does not match the required format: <samp>${args}</samp>`;
                        }
                        break;
                    case 'forbiddenIf':
                        if (isSet() && isSet(args)) {
                            message = `<samp>${config.name}</samp> and <samp>${args}</samp> cannot both be set`;
                        }
                        break;
                    case 'forbiddenUnless':
                        if (isSet() && !isSet(args)) {
                            message = `<samp>${config.name}</samp> can only be set when <samp>${args}</samp> is set`;
                        }
                        break;
                    case 'requiredIf':
                        if (!isSet() && isSet(args)) {
                            message = `<samp>${config.name}</samp> must be set if <samp>${args}</samp> is set`;
                        }
                        break;
                    case 'requiredUnless':
                        if (!isSet() && !isSet(args)) {
                            message = `<samp>${config.name}</samp> or <samp>${args}</samp> is required`;
                        }
                        break;
                    case 'requiredUnlessAnyOf':
                        if (!isSet() && !isAnySet(args)) {
                            message = `<samp>${config.name}</samp> or one of <samp>${args}</samp> is required`;
                        }
                        break;
                    case 'forbiddenUnlessAnyOf':
                        if (isSet() && !isAnySet(args)) {
                            message = `<samp>${config.name}</samp> cannot be set if any of <samp>${args}</samp> are set`;
                        }
                        break;
                    default:
                        $log.warn("Unknown constraint predicate", constraintO, config);
                }
                if (message !== null) {
                    entity.addIssue(Issue.builder().group('config').ref(config.name).message($sce.trustAsHtml(message)).build());
                }
            }
        }
        return $q((resolve) => {
            if (entity.miscData.has('config')) {
                entity.miscData.get('config')
                    .forEach(checkSensitiveFields);

                entity.miscData.get('config')
                    .filter(config => config.constraints && config.constraints.length > 0)
                    .forEach(checkConstraints);
            }
            // could do same as above to check parameters, but that doesn't make the parameters appear as config to set,
            // so instead we merge parameters with config
            resolve();
        });
    }

    function refreshConfigMemberspecsMetadata(entity) {
        let promiseArray = [];
        Object.values(entity.getClusterMemberspecEntities()).forEach((memberSpec)=> {
            // memberSpec can be `undefined` if the member spec is not a `$brooklyn:entitySpec`, e.g. it is `$brooklyn:config("spec")`;
            // only accept for the former, where refresh will have replaced it with an Entity object
            if (memberSpec instanceof Entity) {
                promiseArray.push(refreshBlueprintMetadata(memberSpec, 'SPEC'));
            }
        });
        return $q.all(promiseArray);
    }

    function refreshPoliciesMetadata(entity) {
        return $q.all(entity.getPoliciesAsArray().reduce((result, policy)=> {
            policy.miscData.set('loading', true);
            policy.family = 'POLICY';

            let deferred = $q.defer();

            paletteApi.getType(policy.type, policy.version).then((data)=> {
                deferred.resolve(populateEntityFromApiSuccess(policy, data));
            }).catch(function (error) {
                deferred.resolve(populateEntityFromApiError(policy, error));
            }).finally(()=> {
                policy.miscData.set('loading', false);
            });
            result.push(deferred);
            return result;
        }, []));
    }

    function refreshEnrichersMetadata(entity) {
        return $q.all(entity.getEnrichersAsArray().reduce((result, enricher)=> {
            enricher.miscData.set('loading', true);
            enricher.family = 'ENRICHER';

            let deferred = $q.defer();

            paletteApi.getType(enricher.type, enricher.version).then((data)=> {
                deferred.resolve(populateEntityFromApiSuccess(enricher, data));
            }).catch(function (error) {
                deferred.resolve(populateEntityFromApiError(enricher, error));
            }).finally(()=> {
                enricher.miscData.set('loading', false);
            });
            result.push(deferred);
            return result;
        }, []));
    }

    function refreshConfigInherited(entity) {
        return $q((resolve) => {
            (Array.isArray(entity.miscData.get('config')) ? entity.miscData.get('config') : [])
                .filter(definition => !entity.config.has(definition.name))
                .forEach(definition => {
                    if (entity.hasInheritedConfig(definition.name)) {
                        entity.addIssue(Issue.builder()
                            .group('config')
                            .ref(definition.name)
                            .level(ISSUE_LEVEL.WARN)
                            .message(`Implicitly defined (inherited from an ancestor)`)
                            .build());
                    }
                });
            resolve();
        });
    }

    function parseInput(input, entity) {
        return $q((resolve, reject) => {
            try {
                let parsed = dslService.parse(input, entity, getBlueprint());
                if (parsed.kind && parsed.kind.family === 'constant') {
                    reject('constants not interpreted as DSL when parsed', input);
                } else {
                    resolve(parsed);
                }
            } catch (ex) {
                $log.debug("Cannot detect whether this is a DSL expression; assuming not", input, ex);
                reject(ex, input);
            }
        });
    }

    function refreshRelationships(entity) {
        return $q.all(Array.from(entity.config.keys()).reduce((promises, key) => {
            let value = entity.config.get(key);

            // Return promises that returns an object like { key: "my-config-key-key", issues: [] }
            if (value instanceof Dsl) {
                promises.push(
                    parseInput(value.toString(), entity).then(dsl => {
                        entity.config.set(key, dsl);
                        return {
                            key: key,
                            issues: dsl.getAllIssues()
                        };
                    }).catch(() => $q.resolve({
                        key: key,
                        issues: []
                    }))
                );
            } else if (value instanceof Array) {
                promises.push(
                    $q.all(value.reduce((issues, itemValue, itemIndex) => {
                        return issues.concat(
                            parseInput(itemValue, entity).then(dsl => {
                                value[itemIndex] = dsl;
                                return dsl.getAllIssues();
                            }).catch(() => [])
                        );
                    }, [])).then(issues => {
                        return {
                            key: key,
                            issues: issues.reduce((acc, issue) => acc.concat(issue), [])
                        }
                    })
                );
            } else if (value instanceof Object) {
                promises.push(
                    $q.all(Object.keys(value).reduce((issues, itemKey) => {
                        return issues.concat(
                            parseInput(value[itemKey], entity).then(dsl => {
                                value[itemKey] = dsl;
                                return dsl.getAllIssues();
                            }).catch(() => [])
                        );
                    }, [])).then(issues => {
                        return {
                            key: key,
                            issues: issues.reduce((acc, issue) => acc.concat(issue), [])
                        }
                    })
                );
            } else {
                promises.push(
                    parseInput(value, entity).then(dsl => {
                        entity.config.set(key, dsl);
                        return {
                            key: key,
                            issues: dsl.getAllIssues()
                        };
                    }).catch(() => $q.resolve({
                        key: key,
                        issues: []
                    }))
                );
            }

            return promises;
        }, [])).then(results => {
            results.forEach(result => {
                entity.clearIssues({ref: result.key, phase: 'relationship'});
                result.issues.forEach(issue => {
                    entity.addIssue(Issue.builder().group('config').phase('relationship').ref(result.key).message($sce.trustAsHtml(issue)).build());
                });
            })
        });
    }

    function refreshAllRelationships(entity = blueprint) {
        let promises = [];
        promises.push(refreshRelationships(entity));
        promises.concat(entity.children.map(child => refreshAllRelationships(child)));

        return $q.all(promises);
    }

    function addConfigKeyDefinition(entity, key) {
        entity.addConfigKeyDefinition(key, false);
    }

    function addParameterDefinition(entity, key) {
        entity.addParameterDefinition(key);
    }

    function addUnlistedConfigKeysDefinitions(entity) {
        // copy config key definitions set on this entity into the miscData aggregated view
        let allConfig = entity.miscDataOrDefault('configMap', {});
        entity.config.forEach((value, key) => {
            entity.addConfigKeyDefinition(key, false, true, value);
        });
        entity.addConfigKeyDefinition(null, false, false);
    }

    function addUnlistedParameterDefinitions(entity) {
        // copy parameter definitions set on this entity into the miscData aggregated view;
        // see discussions in PR 112 about whether this is necessary and/or there is a better way; but note, this is much updated since
        entity.parameters.forEach((param) => {
            entity.addParameterDefinition(param, false, true);
        });
        entity.addParameterDefinition(null, false, false);
    }

    function populateEntityFromApiSuccess(entity, data, parsedPlan) {
        function mapped(list, field) {
            let result = {};
            if (list) {
                list.forEach(l => {
                    if (l && l[field]) {
                        result[l[field]] = l;
                    }
                });
            }
            return result;
        }
        entity.clearIssues({group: 'type'});

        entity.type = data.symbolicName;
        entity.icon = entity.metadata.get('iconUrl')
            ? (data.iconUrl || '/v1/catalog/types/'+entity.type+'/'+(entity.version || 'latest')+'/icon')+'?iconUrl='+entity.metadata.get('iconUrl')
            : data.iconUrl || iconGenerator(data.symbolicName);
        entity.miscData.set('important', !!data.iconUrl);
        entity.miscData.set('bundle', {
            symbolicName: data.containingBundle.split(':')[0],
            version: data.containingBundle.split(':')[1]
        });
        entity.miscData.set('typeName', data.displayName || data.symbolicName);
        entity.miscData.set('displayName', data.displayName);
        entity.miscData.set('symbolicName', data.symbolicName);
        entity.miscData.set('description', data.description);

        entity.miscData.set('config', data.config || []);
        entity.miscData.set('configMap', mapped(data.config, 'name'));
        // if a template sets an explicit value for a parameter, use that as the default everywhere
        const configTemplateDefaults =
                (parsedPlan && parsedPlan['brooklyn.config']) ||
                (parsedPlan && parsedPlan.services && parsedPlan.services.length==1 && parsedPlan.services[0]['brooklyn.config']) ||
                {};
        entity.miscData.set('configTemplateDefaults', configTemplateDefaults);
        (data.config || []).forEach(item => {
            const override = configTemplateDefaults[item.name];
            if (typeof override !== 'undefined') {
                item.defaultValueParameter = item.defaultValue;
                item.defaultValueTemplate = override;
                item.defaultValue = override;
            }
        });
        entity.miscData.set('parameters', data.parameters || []);
        entity.miscData.set('parametersMap', mapped(data.parameters, 'name'));

        entity.miscData.set('sensors', data.sensors || []);
        entity.miscData.set('traits', data.supertypes || []);
        entity.miscData.set('tags', data.tags || []);
        entity.miscData.set('loadedVersion', data.version);

        data.tags.forEach( (t) => {
            mergeAppendingLists(COMMON_HINTS, t['ui-composer-hints']);
        });
        entity.miscData.set('ui-composer-hints', COMMON_HINTS);
        entity.miscData.set('virtual', data.virtual || null);
        addUnlistedConfigKeysDefinitions(entity);
        addUnlistedParameterDefinitions(entity);
        return entity;
    }
    function mergeAppendingLists(dst, src) {
        for (let p in src) {
            if (Array.isArray(dst[p]) || Array.isArray(src[p])) {
                dst[p] = [].concat(dst[p] || [], src[p]);
            } else {
                dst[p] = Object.assign({}, dst[p], src[p]);
            }
        }
        return dst;
    }

    function populateEntityFromApiError(entity, error) {
        $log.warn("Error loading/populating type, data will be incomplete.", entity, error);
        entity.clearIssues({group: 'type'});
        entity.addIssue(Issue.builder().group('type').message($sce.trustAsHtml(`Type <samp>${entity.type + (entity.hasVersion ? ':' + entity.version : '')}</samp> does not exist`)).build());
        entity.miscData.set('typeName', entity.type || '');
        entity.miscData.set('config', []);
        entity.miscData.set('parameters', []);
        entity.miscData.set('sensors', []);
        entity.miscData.set('traits', []);
        entity.miscData.set('virtual', null);
        entity.icon = typeNotFoundIcon;
        addUnlistedConfigKeysDefinitions(entity);
        addUnlistedParameterDefinitions(entity);
        return entity;
    }

    function populateLocationFromApiCommon(entity, data) {
        entity.clearIssues({group: 'location'});
        entity.location = data.yamlHere || data.id;

        let name = data.name || data.displayName;
        if (!name && data.yamlHere) {
            name = typeof data.yamlHere === 'object' ? Object.keys(data.yamlHere)[0] : data.yamlHere;
        }
        if (!name) name =  data.symbolicName;
        entity.miscData.set('locationName', name);

        // use icon on item, but if none then generate using *yaml* to distinguish when someone has changed it
        // (especially for things like jclouds:aws-ec2 -- the config is more interesting than the type name)
        entity.miscData.set('locationIcon', data==null ? null : data.iconUrl || iconGenerator(data.yamlHere ? JSON.stringify(data.yamlHere) : data.symbolicName));
        return entity;
    }

    function populateLocationFromApiSuccess(entity, data) {
        populateLocationFromApiCommon(entity, data);
    }

    function populateLocationFromApiError(entity) {
        populateLocationFromApiCommon(entity, { yamlHere: entity.location });
        entity.addIssue(Issue.builder().level(ISSUE_LEVEL.WARN).group('location').message($sce.trustAsHtml(`Location <samp>${!(entity.location instanceof String) ? JSON.stringify(entity.location) : entity.location}</samp> does not exist in your local catalog. Deployment might fail.`)).build());
        entity.miscData.set('locationIcon', typeNotFoundIcon);
        return entity;
    }

    /**
     * Adds {Entity} relationships provider to discover relationships between entities that are specific to provider.
     *
     * @param {String} providerName The relationships provider name.
     * @param {Object} entityRelationshipsProvider The {Entity} relationships provider. The provider must implement the
     * method `apply({Entity})` which takes {Entity} as an argument and returns array of relationships found in the
     * format [{source: {Entity}, target: {Entity}}], or an empty array [].
     */
    function addEntityRelationshipsProvider(providerName, entityRelationshipsProvider) {
        if (typeof entityRelationshipsProvider.apply !== 'function' || !providerName) {
            console.error(`Provider ${entityRelationshipsProvider} with name ${providerName} is not an Entity relationships provider.`);
        }
        entityRelationshipProviders[providerName] = entityRelationshipsProvider;
    }

    /**
     * Retrieves all the Entities referenced by an Entity.
     *
     * @param {Entity} entity the Entity to resolve relative references from
     * @return {Array} of objects that contains source and target entities
     */
    function getRelationships(entity = blueprint) {
        let relationships = [];

        // Aggregate relationships discovered by Entity relationships providers.
        for (let provider of Object.values(entityRelationshipProviders)) {
            relationships = relationships.concat(provider.apply(entity));
        }

        // Iterate over children and reduct.
        return entity.children.reduce((relationships, child) => {
            return relationships.concat(getRelationships(child))
        }, relationships);
    }

    function populateId(entity) {
        if (entity.id) return;

        let defaultSalterFn = (candidateId, index) => candidateId+"-"+index;
        let uniqueSuffixFn = (candidateId, root, salterFn) => {
            let matches = {};
            root.visitWithDescendants(e => {
                if (e.id && e.id.startsWith(candidateId)) {
                    matches[e.id] = true;
                }
            });
            if (!matches[candidateId]) return candidateId;
            let i=2;
            while (true) {
                let newCandidateId = (salterFn || defaultSalterFn)(candidateId, i);
                if (!matches[newCandidateId]) return newCandidateId;
                i++;
            }
        };

        entity.id = (brBrandInfo.blueprintComposerIdGenerator || blueprintComposerIdGenerator)(entity, uniqueSuffixFn);
    }

    function blueprintComposerIdGenerator(entity, uniqueSuffixFn) {
        let candidate = entity.hasName()
            ? entity.name.replace(/\W/g, '-').toLowerCase()
            : entity.type ? entity.type.replace(/\W/g, '-').toLowerCase()
                : !entity.parent ? "root"
                    : entity._id;
        return uniqueSuffixFn(
            candidate,
            entity.getApplication() /* unique throughout blueprint */,
            null /* use default salter */ );
    }

}