brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From henev...@apache.org
Subject [15/28] brooklyn-ui git commit: This adds the contributed brooklyn-ui-angular code in the folder new/.
Date Thu, 26 Jul 2018 10:50:32 GMT
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/util/model/dsl.model.spec.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/util/model/dsl.model.spec.js b/new/ui-modules/blueprint-composer/app/components/util/model/dsl.model.spec.js
new file mode 100644
index 0000000..836322b
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/util/model/dsl.model.spec.js
@@ -0,0 +1,452 @@
+/*
+ * 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 {Dsl, KIND, DslParser, Tokenizer, DslError} from './dsl.model';
+import {Entity} from './entity.model';
+
+describe('Dsl Component Model', ()=>{
+   describe('Dsl Initialization and expression generation', ()=>{
+       it('should initialize correctly', ()=> {
+           let dsl1 = new Dsl();
+           expect(dsl1._id).not.toBeNull();
+           expect(dsl1.kind).toBe(KIND.STRING);
+           expect(dsl1.hasName()).toBe(true);
+           expect(dsl1.hasParent()).toBe(false);
+           expect(dsl1.hasRef()).toBe(false);
+
+           let dsl2 = new Dsl(KIND.STRING, 'hello');
+           expect(dsl2._id).not.toBeNull();
+           expect(dsl2.kind).toBe(KIND.STRING);
+           expect(dsl2.name).toBe('hello');
+           expect(dsl2.hasParent()).toBe(false);
+           expect(dsl2.hasRef()).toBe(false);
+
+           let dsl3 = new Dsl(KIND.UTILITY, 'formatString');
+           expect(dsl3._id).not.toBeNull();
+           expect(dsl3.kind).toBe(KIND.UTILITY);
+           expect(dsl3.name).toBe('formatString');
+       });
+
+       it('should set and get name correctly', ()=> {
+           let dsl = new Dsl();
+           dsl.name = 'Test' + 'Name';
+           expect(dsl.name).toBe('TestName');
+       });
+
+       it('should set and get ref correctly', ()=> {
+           let entity = new Entity();
+           const CONFIG_OBJECT = {
+               textKey: 'textValue',
+               boolKey: false,
+               numKey: 123456789,
+               nullKey: null,
+               objectKey: {
+                   key: 'val',
+               }
+           };
+           entity.setEntityFromJson(CONFIG_OBJECT);
+
+           let dsl = new Dsl();
+           dsl.ref = entity;
+
+           expect(dsl.ref).toEqual(entity);
+           expect(dsl.kind).toBe(KIND.ENTITY);
+       });
+
+       it('should set and get parent hierarchy correctly', ()=> {
+           let dsl = new Dsl();
+           let child = new Dsl();
+           let grandchild = new Dsl();
+
+           child.parent = dsl;
+           grandchild.parent = child;
+
+           expect(child.parent).toBe(dsl);
+           expect(grandchild.parent).toBe(child);
+           expect(grandchild.parent.parent).toBe(dsl);
+           expect(grandchild.parent.parent.parent).toBeUndefined();
+
+           expect(dsl.getRoot()).toBe(dsl);
+           expect(child.getRoot()).toBe(dsl);
+           expect(grandchild.getRoot()).toBe(dsl);
+       });
+
+       it('should handle params correctly', ()=> {
+           let dsl = new Dsl(KIND.METHOD);
+
+           expect(dsl.params.length).toBe(0);
+
+           let param1 = new Dsl();
+           let param2 = new Dsl();
+           let param3 = new Dsl();
+
+           dsl.param(param1);
+           dsl.param(param2);
+           dsl.param(param3);
+
+           expect(dsl.params).toEqual([param1, param2, param3]);
+
+           expect(param1.parent).toBe(dsl);
+           expect(param1.getRoot()).toBe(dsl);
+           expect(param2.parent).toBe(dsl);
+           expect(param2.getRoot()).toBe(dsl);
+           expect(param3.parent).toBe(dsl);
+           expect(param3.getRoot()).toBe(dsl);
+       });
+
+       it('should handle prev and next correctly', ()=> {
+           let dsl = new Dsl(KIND.METHOD);
+
+           expect(dsl.next).toBeUndefined();
+           expect(dsl.prev).toBeUndefined();
+
+           let meth1 = new Dsl(KIND.METHOD);
+           let meth2 = new Dsl(KIND.METHOD);
+           let meth3 = new Dsl(KIND.METHOD);
+
+           dsl.chain(meth1);
+           expect(dsl.next).toBe(meth1);
+           expect(meth1.prev).toBe(dsl);
+
+           dsl.chain(meth2).chain(meth3);
+           expect(dsl.prev).toBeUndefined();
+           expect(dsl.next).toBe(meth1);
+           expect(dsl.getLastMethod()).toBe(meth3);
+           expect(meth1.prev).toBe(dsl);
+           expect(meth1.next).toBe(meth2);
+           expect(meth2.prev).toBe(meth1);
+           expect(meth2.next).toBe(meth3);
+           expect(meth3.prev).toBe(meth2);
+           expect(meth3.next).toBeUndefined();
+
+           dsl.popChainedMethod();
+           expect(dsl.prev).toBeUndefined();
+           expect(dsl.next).toBe(meth1);
+           expect(dsl.getLastMethod()).toBe(meth2);
+           expect(meth1.prev).toBe(dsl);
+           expect(meth1.next).toBe(meth2);
+           expect(meth2.prev).toBe(meth1);
+           expect(meth2.next).toBeUndefined();
+
+           expect(dsl.equals(dsl));
+       });
+
+       it('should generate YAML correctly', ()=> {
+           // constants
+           let dsl1 = new Dsl(KIND.STRING, 'hello world');
+
+           expect(dsl1.toString()).toEqual('"hello world"');
+           expect(dsl1.generate()).toEqual('"hello world"');
+
+           let dsl2 = new Dsl(KIND.NUMBER, 10.2);
+
+           expect(dsl2.toString()).toEqual('10.2');
+           expect(dsl2.generate()).toEqual('10.2');
+
+           // target function without parameters
+           let dsl3 = new Dsl(KIND.TARGET, 'self');
+           expect(dsl3.generateParams()).toEqual('');
+           expect(dsl3.toString()).toEqual('$brooklyn:self()');
+           expect(dsl3.generate()).toEqual('self()');
+
+           // method with one parameter
+           let awr1 = new Dsl(KIND.METHOD, 'attributeWhenReady');
+           let par1 = new Dsl(KIND.STRING, 'http.port');
+           awr1.param(par1);
+           dsl3.chain(awr1);
+           expect(awr1.generate()).toEqual('attributeWhenReady("http.port")');
+
+           // the full DSL expression
+           expect(dsl3.toString()).toEqual('$brooklyn:self().attributeWhenReady("http.port")');
+
+           // chained target functions and methods: parent().sibling(...).attributeWhenReady(...)
+           let dsl4 = new Dsl(KIND.TARGET, 'parent');
+           let sib1 = new Dsl(KIND.TARGET, 'sibling');
+           let par2 = new Dsl(KIND.STRING, 'rootNode');
+           sib1.param(par2);
+           dsl4.chain(sib1);
+           expect(sib1.generate()).toEqual('sibling("rootNode")');
+
+           let awr2 = new Dsl(KIND.METHOD, 'attributeWhenReady');
+           let par3 = new Dsl(KIND.STRING, 'http.port');
+           awr2.param(par3);
+           dsl4.chain(awr2);
+           expect(awr2.generate()).toEqual('attributeWhenReady("http.port")');
+
+           expect(dsl4.toString()).toEqual('$brooklyn:parent().sibling("rootNode").attributeWhenReady("http.port")');
+
+           // utility function with multiple parameters
+           let func1 = new Dsl(KIND.UTILITY, 'formatString');
+           let s1 = new Dsl(KIND.STRING, '%s:%s');
+           let s2 = new Dsl(KIND.STRING, 'localhost');
+           let s3 = new Dsl(KIND.STRING, '8081');
+           func1.param(s1);
+           func1.param(s2);
+           func1.param(s3);
+           expect(func1.generate()).toEqual('formatString("%s:%s", "localhost", "8081")');
+
+           // the full DSL expression
+           expect(func1.toString()).toEqual('$brooklyn:formatString("%s:%s", "localhost", "8081")');
+
+           // utility function with multiple parameters and nested calls
+           let func2 = new Dsl(KIND.UTILITY, 'formatString');
+           let ss1 = new Dsl(KIND.STRING, '%s:%s');
+           let ss2 = new Dsl(KIND.STRING, 'localhost');
+           func2.param(ss1);
+           func2.param(ss2);
+           func2.param(dsl4);
+           expect(func2.generate()).toEqual('formatString("%s:%s", "localhost", $brooklyn:parent().sibling("rootNode").attributeWhenReady("http.port"))');
+
+           // the full DSL expression
+           expect(func2.toString()).toEqual('$brooklyn:formatString("%s:%s", "localhost", $brooklyn:parent().sibling("rootNode").attributeWhenReady("http.port"))');
+
+           // utility function with multiple port parameters
+           let func3 = new Dsl(KIND.UTILITY, 'formatString');
+           let sss1 = new Dsl(KIND.STRING, 'Using ports %s or %s');
+           let sss2 = new Dsl(KIND.PORT, '8080+');
+           let sss3 = new Dsl(KIND.PORT, '8080-10010');
+
+           // The ports only
+           expect(sss2.generate()).toEqual('8080+');
+           expect(sss2.toString()).toEqual('8080+');
+           expect(sss3.generate()).toEqual('8080-10010');
+           expect(sss3.toString()).toEqual('8080-10010');
+
+           func3.param(sss1);
+           func3.param(sss2);
+           func3.param(sss3);
+           expect(func3.generate()).toEqual('formatString("Using ports %s or %s", "8080+", "8080-10010")');
+
+           // the full DSL expression
+           expect(func3.toString()).toEqual('$brooklyn:formatString("Using ports %s or %s", "8080+", "8080-10010")');
+       });
+
+       it('should handle equals correctly', ()=> {
+           let dsl1 = new Dsl(KIND.METHOD, 'hello');
+           let dsl2 = new Dsl(KIND.METHOD, 'hello');
+
+           expect(dsl1.equals(dsl2)).toBe(true);
+           expect(dsl2.equals(dsl1)).toBe(true);
+
+           let meth1 = new Dsl(KIND.METHOD, "print");
+           let meth2 = new Dsl(KIND.METHOD, "print");
+
+           dsl1.chain(meth1);
+           dsl2.chain(meth2);
+
+           expect(dsl1.equals(dsl2)).toBe(true);
+           expect(dsl2.equals(dsl1)).toBe(true);
+
+           let param1 = new Dsl(KIND.STRING, "world");
+           meth1.param(param1);
+
+           let param2 = new Dsl(KIND.STRING, "world");
+           meth2.param(param2);
+
+           expect(dsl1.equals(dsl2)).toBe(true);
+
+           expect(meth1.equals(meth2)).toBe(true);
+
+       });
+   });
+});
+
+describe('Dsl parser', ()=> {
+
+    describe('Tokenizer', () => {
+        let qs = '"hello world"'; // a quoted string
+        let words = 'word1 word2   word3  ';
+        let wsep = 'word1  , word2, word3 ,word4   ';
+
+        it('should initialize correctly', () => {
+            let t = new Tokenizer(qs);
+        });
+
+        it('should tokenize a double-quoted string', () => {
+            let t = new Tokenizer(qs);
+            expect(t.atEndOfInput()).toBe(false);
+            expect(t.peek('"')).toBe(true);
+            expect(t.nextQuotedString()).toEqual(qs);
+        });
+
+        it('should tokenize a single-quoted string', () => {
+            let sqs = "'I Love Single ''Quotes'''";
+            let t = new Tokenizer(sqs);
+            expect(t.nextSingleQuotedString()).toEqual(sqs);
+        });
+
+        it('should tokenize words', () => {
+            let t = new Tokenizer(words);
+            expect(t.atEndOfInput()).toBe(false);
+            expect(t.peek('"')).toBe(false);
+            expect(t.nextIdentifier()).toEqual('word1');
+            expect(t.nextIdentifier()).toEqual('word2');
+            expect(t.nextIdentifier()).toEqual('word3');
+            expect(t.atEndOfInput()).toBe(true);
+        });
+
+        it('should tokenize words with separators', () => {
+            let t = new Tokenizer(wsep);
+            expect(t.atEndOfInput()).toBe(false);
+            expect(t.nextIdentifier()).toEqual('word1');
+            expect(t.next(',')).toEqual(',');
+            expect(t.nextIdentifier()).toEqual('word2');
+            expect(t.next(',')).toEqual(',');
+            expect(t.nextIdentifier()).toEqual('word3');
+            expect(t.next(',')).toEqual(',');
+            expect(t.nextIdentifier()).toEqual('word4');
+            expect(t.atEndOfInput()).toBe(true);
+        });
+    });
+
+    describe('DslParser', () => {
+
+        it('should initialize correctly', () => {
+            let p = new DslParser("test");
+        });
+
+        it('should parse a string literal', () => {
+            let p = new DslParser('"Hello"');
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.STRING);
+            expect(dsl.name).toEqual('Hello');
+        });
+
+        it('should parse a string literal in double quotes containing escaped quotes', () => {
+            let cstr2 = '"This is \\"quoted\\""';
+            let p = new DslParser(cstr2);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.STRING);
+            expect(dsl.name).toEqual('This is "quoted"');
+            expect(dsl.toString()).toEqual(cstr2); // round-trip check
+        });
+
+        it('should parse a string literal in single quotes containing escaped quotes', () => {
+            let cstrYAML = "'This \"thing\" is ''quoted'''"; // YAML: 'This "thing" is ''quoted'''
+            let p = new DslParser(cstrYAML);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.STRING);
+            expect(dsl.name).toEqual("This \"thing\" is 'quoted'");
+            expect(dsl.toString()).toEqual('"This \\"thing\\" is \'quoted\'"'); // JSON: "This \"thing\" is 'quoted'"
+        });
+
+        it('should parse a variety of number literals', () => {
+            let nums = ['1', '123', '-1', '-123',
+                '.345', '-.345', '0.', '0.0', '-0.01', '+0.01',
+                '9999999999999.123', '-9999999999999.123'];
+            for (let cnum of nums) {
+                let p = new DslParser(cnum);
+                let dsl = p.parse();
+                expect(dsl).toBeDefined();
+                expect(dsl.kind).toBe(KIND.NUMBER);
+                expect(dsl.name).toEqual(Number(cnum).toString());
+                expect(dsl.toString()).toEqual(Number(cnum).toString());
+            }
+        });
+
+        it('should parse a variety of port ranges', () => {
+            let portRanges = ['0+', '8080+', '0-65535', '1024-4096'];
+            for (let range of portRanges) {
+                let p = new DslParser(range);
+                let dsl = p.parse();
+                expect(dsl).toBeDefined();
+                expect(dsl.kind).toBe(KIND.PORT);
+                expect(dsl.name).toEqual(range);
+                expect(dsl.toString()).toEqual(range);
+            }
+        });
+
+        it('should parse a utility function call with no params', () => {
+            let expr1 = '$brooklyn:formatString()';
+            let p = new DslParser(expr1);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.UTILITY);
+            expect(dsl.name).toEqual('formatString');
+            expect(dsl.params.length).toBe(0);
+            expect(dsl.toString()).toEqual(expr1); // round-trip check
+        });
+
+        it('should parse a utility function call with one param', () => {
+            let expr2 = '$brooklyn:formatString("hello")';
+            let p = new DslParser(expr2);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.UTILITY);
+            expect(dsl.name).toEqual('formatString');
+            expect(dsl.params.length).toBe(1);
+            expect(dsl.params[0].kind).toBe(KIND.STRING);
+            expect(dsl.toString()).toEqual(expr2); // round-trip check
+        });
+
+        it('should parse a utility function call with params', () => {
+            let expr3 = '$brooklyn:formatString("%s", "hello")';
+            let p = new DslParser(expr3);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.UTILITY);
+            expect(dsl.name).toEqual('formatString');
+            expect(dsl.params.length).toBe(2);
+            expect(dsl.toString()).toEqual(expr3); // round-trip check
+        });
+
+        it('should parse a method function call with params', () => {
+            let method_expr = '$brooklyn:attributeWhenReady("sensor1")';
+            let p = new DslParser(method_expr);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.METHOD);
+            expect(dsl.name).toEqual('attributeWhenReady');
+            expect(dsl.params.length).toBe(1);
+            expect(dsl.toString()).toEqual(method_expr); // round-trip check
+        });
+
+        it('should parse a target function call with no params', () => {
+            let target_expr = '$brooklyn:parent()';
+            let p = new DslParser(target_expr);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.kind).toBe(KIND.TARGET);
+            expect(dsl.name).toEqual('parent');
+            expect(dsl.params.length).toBe(0);
+            expect(dsl.toString()).toEqual(target_expr); // round-trip check
+        });
+
+        it('should parse a complex function call', () => {
+            let target_expr = '$brooklyn:formatString("%s:%s", "localhost", $brooklyn:parent().sibling("rootNode").attributeWhenReady("http.port"))';
+            let p = new DslParser(target_expr);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.toString()).toEqual(target_expr); // round-trip check
+        });
+
+        it('should get references', () => {
+            let target_expr = '$brooklyn:formatString("%s:%s", $brooklyn:component("db").attributeWhenReady("host.address"), $brooklyn:component("nginx").attributeWhenReady("http.port"))';
+            let p = new DslParser(target_expr);
+            let dsl = p.parse();
+            expect(dsl).toBeDefined();
+            expect(dsl.toString()).toEqual(target_expr); // round-trip check
+            expect(dsl.getReferences().length).toEqual(2);
+        });
+
+    });
+});
+

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.js b/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.js
new file mode 100644
index 0000000..9f0b5c7
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.js
@@ -0,0 +1,1093 @@
+/*
+ * 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$/;
+const ANY_MEMBERSPEC_REGEX = /^(\w+\.)+(first)?[mM]ember[sS]pec$/;
+const RESERVED_KEY_REGEX = /(^children$|^services$|^locations?$|^brooklyn\.config$|^brooklyn\.enrichers$|^brooklyn\.policies$)/;
+const FIELD = {
+    SERVICES: 'services', CHILDREN: 'brooklyn.children', CONFIG: 'brooklyn.config', 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 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());
+        METADATA.set(this, new Map());
+        ENRICHERS.set(this, new Map());
+        POLICIES.set(this, new Map());
+        CHILDREN.set(this, new Array());
+        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() {
+        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.touch();
+    }
+
+    /**
+     * Remove {Entity} location
+     * @returns {string}
+     */
+    removeLocation() {
+        LOCATIONS.delete(this);
+        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 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 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.getMetadataAsJson = getMetadataAsJson;
+Entity.prototype.setMetadataFromJson = setMetadataFromJson;
+
+Entity.prototype.setEnrichersFromJson = setEnrichersFromJson;
+Entity.prototype.setPoliciesFromJson = setPoliciesFromJson;
+
+Entity.prototype.getData = getData;
+Entity.prototype.addConfig = addConfig;
+Entity.prototype.addMetadata = addMetadata;
+Entity.prototype.removeConfig = removeConfig;
+Entity.prototype.removeMetadata = removeMetadata;
+Entity.prototype.isCluster = isCluster;
+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 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 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 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)=>(config.type === 'org.apache.brooklyn.api.entity.EntitySpec'))
+        .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)=>(config.type === 'org.apache.brooklyn.api.entity.EntitySpec'))
+        .reduce((acc, config)=> {
+            if (CONFIG.get(this).has(config.name) && predicate(config, CONFIG.get(this).get(config.name)[DSL.ENTITY_SPEC])) {
+                return CONFIG.get(this).get(config.name)[DSL.ENTITY_SPEC];
+            }
+            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)=>(config.type === 'org.apache.brooklyn.api.entity.EntitySpec' && 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.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());
+    METADATA.set(this, new Map());
+    ENRICHERS.set(this, new Map());
+    POLICIES.set(this, new Map());
+    CHILDREN.set(this, new Array());
+    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.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')
+    }
+    var children = new Array();
+
+    incomingModel.reduce((self, child)=> {
+        var 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();
+    var self = this;
+    Object.keys(incomingModel).forEach((key)=>(self.addConfig(key, incomingModel[key])));
+    this.touch();
+}
+
+
+function setMetadataFromJson(incomingModel) {
+    METADATA.get(this).clear();
+    var 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);
+}
+
+/* "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;
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.spec.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.spec.js b/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.spec.js
new file mode 100644
index 0000000..ab53f50
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/util/model/entity.model.spec.js
@@ -0,0 +1,346 @@
+/*
+ * 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 {Entity, EntityFamily, PREDICATE_MEMBERSPEC, PREDICATE_FIRST_MEMBERSPEC} from "./entity.model";
+import {Issue} from './issue.model';
+
+describe('Brooklyn Model', ()=> {
+
+    describe('Entity', ()=> {
+        it('should initialize correctly', ()=> {
+            let entity = new Entity();
+            expect(entity._id).not.toBeNull();
+            expect(entity.hasParent()).toBe(false);
+            expect(entity.hasName()).toBe(false);
+            expect(entity.hasType()).toBe(false);
+            expect(entity.hasVersion()).toBe(false);
+            expect(entity.hasChildren()).toBe(false);
+            expect(entity.hasConfig()).toBe(false);
+            expect(entity.hasLocation()).toBe(false);
+            expect(entity.hasInheritedLocation()).toBe(false);
+            expect(entity.hasEnrichers()).toBe(false);
+            expect(entity.hasPolicies()).toBe(false);
+            expect(entity.hasIssues()).toBe(false);
+        });
+
+        it('should load metadata from JSON', ()=> {
+            let entity = new Entity();
+            entity.setEntityFromJson(CONFIG_OBJECT);
+            let data = entity.getData();
+            expect(data).not.toBeNull();
+            expect(Object.keys(data)).toEqual(Object.keys(CONFIG_OBJECT));
+        });
+
+        it('should fail to load catalog.bom JSON', ()=> {
+            let entity = new Entity();
+            expect(()=>(entity.setEntityFromJson({'brooklyn.catalog': {}})))
+                .toThrowError('Catalog format not supported ... unsupported field [brooklyn.catalog]');
+            expect(()=>(entity.setEntityFromJson({'item': {}})))
+                .toThrowError('Catalog format not supported ... unsupported field [item]');
+            expect(()=>(entity.setEntityFromJson({'items': {}})))
+                .toThrowError('Catalog format not supported ... unsupported field [items]');
+        });
+
+        it('should fail to load an invalid JSON type', ()=> {
+            let msgPattern = /^Entity cannot be set from \[([A-Za-z]*)] ... please supply an \[Object]$/;
+            let entity = new Entity();
+            expect(()=>(entity.setEntityFromJson('string')))
+                .toThrowError(msgPattern);
+            expect(()=>(entity.setEntityFromJson(['array'])))
+                .toThrowError(msgPattern);
+            expect(()=>(entity.setEntityFromJson(true)))
+                .toThrowError(msgPattern);
+            expect(()=>(entity.setEntityFromJson(12345)))
+                .toThrowError(msgPattern);
+        });
+
+        it('should load brooklyn.config from JSON', ()=> {
+            let entity = new Entity();
+            entity.setConfigFromJson(CONFIG_OBJECT);
+            let data = entity.getData();
+            expect(data).not.toBeNull();
+            expect(Object.keys(data)).toContain('brooklyn.config');
+            expect(Object.keys(data['brooklyn.config'])).toEqual(Object.keys(CONFIG_OBJECT));
+        });
+
+        it('should load blueprint from JSON', ()=> {
+            let entity = new Entity();
+            entity.setEntityFromJson(BLUEPRINT_OBJECT);
+            let data = entity.getData();
+            expect(data).not.toBeNull();
+            expect(Object.keys(data)).toEqual(Object.keys(BLUEPRINT_OBJECT));
+            expect(entity.location).toEqual(BLUEPRINT_OBJECT.location);
+            expect(data.services.length).toBe(BLUEPRINT_OBJECT.services.length);
+            expect(Object.keys(data.services[0])).toEqual(Object.keys(BLUEPRINT_OBJECT.services[0]));
+            expect(data.services[0]['brooklyn.children'].length)
+                .toBe(BLUEPRINT_OBJECT.services[0]['brooklyn.children'].length);
+            expect(data.services[0]['brooklyn.children'][0].type)
+                .toBe(BLUEPRINT_OBJECT.services[0]['brooklyn.children'][0].type);
+            expect(data.services[0]['brooklyn.policies'].length)
+                .toBe(BLUEPRINT_OBJECT.services[0]['brooklyn.policies'].length);
+            expect(data.services[0]['brooklyn.policies'][0].type)
+                .toBe(BLUEPRINT_OBJECT.services[0]['brooklyn.policies'][0].type);
+            expect(data.services[0]['brooklyn.enrichers'].length)
+                .toBe(BLUEPRINT_OBJECT.services[0]['brooklyn.enrichers'].length);
+            expect(data.services[0]['brooklyn.enrichers'][0].type)
+                .toBe(BLUEPRINT_OBJECT.services[0]['brooklyn.enrichers'][0].type);
+        });
+
+        it('should be a "clustered" entity if it has "cluster" or "group" traits', ()=> {
+            let entity = new Entity();
+            expect(entity.isCluster()).toBe(false);
+            entity.miscData.set('traits', ['com.example.MyChildType0']);
+            expect(entity.isCluster()).toBe(false);
+            entity.miscData.set('traits', ['com.example.MyChildType0', 'org.apache.brooklyn.entity.group.Cluster']);
+            expect(entity.isCluster()).toBe(true);
+            entity.miscData.set('traits', ['com.example.MyChildType0', 'org.apache.brooklyn.entity.group.Fabric']);
+            expect(entity.isCluster()).toBe(true);
+        });
+        it('should retrieve the cluster memberspec entities', ()=> {
+            let entity = new Entity();
+            entity.setEntityFromJson(BLUEPRINT_OBJECT);
+            entity.children[1].miscData.set('traits', TRAITS_CLUSTER);
+            entity.children[1].miscData.set('config', AVAILABLE_CONFIG);
+            expect(entity.children.length).toBe(BLUEPRINT_OBJECT.services.length);
+            expect(entity.children[1].isCluster()).toBe(true);
+            let memberSpecs = Object.values(entity.children[1].getClusterMemberspecEntities());
+            expect(memberSpecs).toBeDefined();
+            expect(memberSpecs.length).toBe(2);
+            expect(memberSpecs[0].family).toBe(EntityFamily.SPEC);
+            expect(memberSpecs[1].family).toBe(EntityFamily.SPEC);
+            expect(entity.children[1].getClusterMemberspecEntity((config, entity)=>(entity._id === memberSpecs[0]._id))).toBe(memberSpecs[0]);
+            expect(entity.children[1].getClusterMemberspecEntity((config, entity)=>(entity._id === memberSpecs[1]._id))).toBe(memberSpecs[1]);
+            expect(entity.children[1].getClusterMemberspecEntity(PREDICATE_MEMBERSPEC)).toBe(memberSpecs[0]);
+            expect(entity.children[1].getClusterMemberspecEntity(PREDICATE_FIRST_MEMBERSPEC)).toBe(memberSpecs[1]);
+            let configKeys = Object.keys(entity.children[1].getClusterMemberspecEntities());
+            expect(configKeys).toBeDefined();
+            expect(configKeys.length).toBe(2);
+            expect(configKeys[0]).toBe('cluster.memberspec');
+            expect(configKeys[1]).toBe('cluster.firstMemberspec');
+        });
+
+        // Location
+        it('should have an inherited location', ()=> {
+            let entity = new Entity();
+            entity.setEntityFromJson(BLUEPRINT_OBJECT);
+            if (entity.hasChildren()) {
+                checkInheritedLocation(entity, entity.location);
+            }
+
+            function checkInheritedLocation(entity, location) {
+                entity.children.forEach((child)=> {
+                    expect(child.hasInheritedLocation()).toBeTruthy();
+                    expect(child.getInheritedLocation()).toBe(location);
+                    expect(child.getInheritedLocation()).toBe(BLUEPRINT_OBJECT.location);
+                    if (child.hasChildren()) {
+                        checkInheritedLocation(child, location);
+                    }
+                });
+            }
+        });
+
+        // Policies
+        it('should not add a policy if not an Entity', ()=> {
+            let addPolicy = ()=> {
+                let entity = new Entity();
+                let policy = new Object();
+                entity.addPolicy(policy);
+            };
+
+            expect(addPolicy).toThrowError('Cannot add policy ... policy must be of type policy');
+        });
+        it('should be able to add a empty new policy', ()=> {
+            let entity = new Entity();
+            entity.addNewPolicy();
+            expect(entity.policies).not.toBeNull();
+            expect(entity.getPoliciesAsArray().length).toBe(1);
+        });
+        it('should add a policy', ()=> {
+            let entity = new Entity();
+            let policy = new Entity();
+            entity.addPolicy(policy);
+            expect(entity.policies).not.toBeNull();
+            expect(entity.getPoliciesAsArray().length).toBe(1);
+            expect(entity.policies.get(policy._id)).toBe(policy);
+        });
+        it('should remove a policy', ()=> {
+            let entity = new Entity();
+            let policy1 = new Entity();
+            let policy2 = new Entity();
+            entity.addPolicy(policy1);
+            entity.addPolicy(policy2);
+            expect(entity.policies).not.toBeNull();
+            expect(entity.getPoliciesAsArray().length).toBe(2);
+            entity.removePolicy(policy1._id);
+            expect(entity.getPoliciesAsArray().length).toBe(1);
+            expect(entity.policies.get(policy2._id)).toBe(policy2)
+        });
+        it('should have the correct family', ()=> {
+            let entity = new Entity();
+            let unknownEntity1 = new Entity();
+            let unknownEntity2 = new Entity();
+            let unknownEntity3 = new Entity();
+            let policy = entity.addNewPolicy();
+            let enricher = entity.addNewEnricher();
+
+            expect(entity.family).toBe(EntityFamily.ENTITY);
+            expect(unknownEntity1.family).toBe(EntityFamily.ENTITY);
+            expect(unknownEntity2.family).toBe(EntityFamily.ENTITY);
+            expect(unknownEntity3.family).toBe(EntityFamily.ENTITY);
+            expect(policy.family).toBe(EntityFamily.POLICY);
+            expect(enricher.family).toBe(EntityFamily.ENRICHER);
+
+            entity.addPolicy(unknownEntity1);
+            entity.addEnricher(unknownEntity2);
+            unknownEntity3.family = 'My totally made up family';
+
+            expect(unknownEntity1.family).toBe(EntityFamily.POLICY);
+            expect(unknownEntity2.family).toBe(EntityFamily.ENRICHER);
+            expect(unknownEntity3.family).toBe(EntityFamily.ENTITY);
+        });
+        it('should be able to add issues', ()=> {
+            let entity = new Entity();
+            entity.addIssue(new Issue());
+
+            expect(entity.issues).toBeDefined();
+            expect(entity.issues.length).toBe(1);
+        });
+        it('should be able to reset all issues', ()=> {
+            let entity = new Entity();
+            entity.addIssue(new Issue());
+            entity.addIssue(new Issue());
+            entity.addIssue(new Issue());
+
+            expect(entity.issues).toBeDefined();
+            expect(entity.issues.length).toBe(3);
+
+            entity.resetIssues();
+
+            expect(entity.issues.length).toBe(0);
+        });
+        it('should be able to reset all issues', ()=> {
+            let entity = new Entity();
+            let issues = [
+                Issue.builder().group('foo').message('foo').build(),
+                Issue.builder().group('bar').message('bar').build(),
+                Issue.builder().group('hello').ref('hello').message('hello').build(),
+                Issue.builder().group('hello').ref('world').message('bar').build(),
+                Issue.builder().group('hello').ref('world').message('bar').build()
+            ];
+            issues.forEach(issue => entity.addIssue(issue));
+
+            expect(entity.issues).toBeDefined();
+            expect(entity.issues.length).toBe(issues.length);
+
+            entity.clearIssues({group: 'foo'});
+
+            expect(entity.issues.length).toBe(4);
+            expect(entity.issues.filter(issue => issue.group === 'foo').length).toBe(0);
+
+            entity.clearIssues({group: 'hello', ref: 'hello'});
+
+            expect(entity.issues.length).toBe(3);
+            expect(entity.issues.filter(issue => issue.group === issue.ref === 'hello').length).toBe(0);
+
+            entity.clearIssues({group: 'hello'});
+
+            expect(entity.issues.length).toBe(1);
+            expect(entity.issues.filter(issue => issue.group === 'hello').length).toBe(0);
+        });
+    });
+});
+
+const TRAITS_CLUSTER = [
+    'org.apache.brooklyn.entity.group.Cluster'
+];
+const AVAILABLE_CONFIG = [{
+        constraints: [],
+        description: "entity spec for creating new cluster members",
+        label: "cluster.memberspec",
+        name: "cluster.memberspec",
+        pinned: false,
+        reconfigurable: false,
+        type: "org.apache.brooklyn.api.entity.EntitySpec",
+    }, {
+        constraints: [],
+        description: "entity spec for creating first cluster member",
+        label: "cluster.firstMemberspec",
+        name: "cluster.firstMemberspec",
+        pinned: false,
+        reconfigurable: false,
+        type: "org.apache.brooklyn.api.entity.EntitySpec",
+    }
+];
+
+const CONFIG_OBJECT = {
+    textKey: 'textValue',
+    boolKey: false,
+    numKey: 123456789,
+    nullKey: null,
+    objectKey: {
+        key: 'val',
+    }
+};
+const BLUEPRINT_OBJECT = {
+    name: 'Blueprint Name',
+    version: '1.0',
+    location: 'my-named-location',
+    services: [
+        {
+            type: 'com.example.MyType',
+            'brooklyn.config': CONFIG_OBJECT,
+            'brooklyn.children': [
+                {
+                    type: 'com.example.MyChildType1',
+                    'brooklyn.config': CONFIG_OBJECT
+                }, {
+                    type: 'com.example.MyChildType2',
+                    'brooklyn.config': CONFIG_OBJECT
+                }
+            ],
+            'brooklyn.policies': [
+                {
+                    type: 'com.example.MyPolicy',
+                    'brooklyn.config': CONFIG_OBJECT
+                }
+            ],
+            'brooklyn.enrichers': [
+                {
+                    type: 'com.example.MyEnricher',
+                    'brooklyn.config': CONFIG_OBJECT
+                }
+            ]
+        }, {
+            type: 'com.example.ClusterType',
+            'brooklyn.config': {
+                'cluster.firstMemberspec': {
+                    '$brooklyn:entitySpec': {
+                        type: 'com.example.first.ClusterMember',
+                        'brooklyn.config': CONFIG_OBJECT
+                    }
+                },
+                'cluster.memberspec': {
+                    '$brooklyn:entitySpec': {
+                        type: 'com.example.ClusterMember',
+                        'brooklyn.config': CONFIG_OBJECT
+                    }
+                }
+            }
+        }
+    ]
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.js b/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.js
new file mode 100644
index 0000000..badfab5
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.js
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+const MESSAGE = new WeakMap();
+const GROUP = new WeakMap();
+const REF = new WeakMap();
+const LEVEL = new WeakMap();
+
+export const ISSUE_LEVEL = {
+    WARN: {
+        id: 'warn',
+        class: 'warn'
+    },
+    ERROR: {
+        id: 'error',
+        class: 'danger'
+    }
+};
+
+export class Issue {
+    constructor() {
+        MESSAGE.set(this, '');
+        GROUP.set(this, '');
+        REF.set(this, '');
+        LEVEL.set(this, ISSUE_LEVEL.ERROR);
+    }
+
+    set message(message) {
+        MESSAGE.set(this, message);
+    }
+
+    get message() {
+        return MESSAGE.get(this);
+    }
+
+    set group(group) {
+        GROUP.set(this, group);
+    }
+
+    get group() {
+        return GROUP.get(this);
+    }
+
+    set ref(ref) {
+        REF.set(this, ref);
+    }
+
+    get ref() {
+        return REF.get(this);
+    }
+
+    set level(level) {
+        LEVEL.set(this, level);
+    }
+
+    get level() {
+        return LEVEL.get(this);
+    }
+
+    static builder() {
+        return new Builder();
+    }
+}
+
+class Builder {
+    constructor() {
+        this.issue = new Issue();
+    }
+
+    message(message) {
+        this.issue.message = message;
+        return this;
+    }
+
+    group(group) {
+        this.issue.group = group;
+        return this;
+    }
+
+    ref(ref) {
+        this.issue.ref = ref;
+        return this;
+    }
+
+    level(level) {
+        this.issue.level = level;
+        return this;
+    }
+
+    build() {
+        if (!this.issue.message || this.issue.message.length === 0) {
+            throw new Error('Issue message is empty');
+        }
+        if (Object.keys(ISSUE_LEVEL).map(key=>ISSUE_LEVEL[key]).indexOf(this.issue.level) === -1) {
+        // if (!Object.keys(ISSUE_LEVEL).map(key=>ISSUE_LEVEL[key]).contains(this.issue.level)) {
+            throw new Error(`"${this.issue.level}" is not a valid issue level (available: [${Object.keys(ISSUE_LEVEL).map(key=>`ISSUES_LEVEL.${key}`).join(', ')}]`)
+        }
+        return this.issue;
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.spec.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.spec.js b/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.spec.js
new file mode 100644
index 0000000..3b52f09
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/util/model/issue.model.spec.js
@@ -0,0 +1,69 @@
+/*
+ * 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, ISSUE_LEVEL} from './issue.model';
+
+describe('Issue model', ()=> {
+    it('can be instantiated correctly', ()=> {
+        let issue = new Issue();
+        expect(issue).toBeDefined();
+        expect(issue.message).toBe('');
+        expect(issue.group).toBe('');
+        expect(issue.ref).toBe('');
+        expect(issue.level).toBe(ISSUE_LEVEL.ERROR);
+    });
+
+    it('can be constructed correctly', ()=> {
+        let message = 'Hello World!';
+        let group = 'config';
+        let ref = 'foo.bar';
+        let level = ISSUE_LEVEL.WARN;
+
+        let issue = Issue.builder().message(message).group(group).ref(ref).level(level).build();
+
+        expect(issue).toBeDefined();
+        expect(issue.message).toBe(message);
+        expect(issue.group).toBe(group);
+        expect(issue.ref).toBe(ref);
+        expect(issue.level).toBe(level);
+    });
+
+    describe('with builder', ()=> {
+        it('cannot instantiate and issue without message', ()=> {
+            let exception;
+            try {
+                Issue.builder().build();
+            } catch (ex) {
+                exception = ex;
+            }
+            expect(exception).toBeDefined();
+            expect(exception.message).toBe('Issue message is empty');
+        });
+        it('cannot instantiate and issue with invalid level', ()=> {
+            let exception;
+            let level = 'foo';
+            try {
+                Issue.builder().message('hello world').level(level).build();
+            } catch (ex) {
+                exception = ex;
+            }
+            expect(exception).toBeDefined();
+            expect(exception.message).toMatch(`"${level}" is not a valid issue level`);
+        });
+    });
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/favicon.ico
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/favicon.ico b/new/ui-modules/blueprint-composer/app/favicon.ico
new file mode 100755
index 0000000..a7e9f26
Binary files /dev/null and b/new/ui-modules/blueprint-composer/app/favicon.ico differ

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/img/icon-add.svg
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/img/icon-add.svg b/new/ui-modules/blueprint-composer/app/img/icon-add.svg
new file mode 100644
index 0000000..4afd183
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/img/icon-add.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:none;}
+	.st1{fill:#FFFFFF;}
+</style>
+<path class="st0" d="M0,0h24v24H0V0z"/>
+<polygon class="st1" points="11.5,16.5 11.5,12.5 7.5,12.5 7.5,11.5 11.5,11.5 11.5,7.5 12.5,7.5 12.5,11.5 16.5,11.5 16.5,12.5 
+	12.5,12.5 12.5,16.5 "/>
+</svg>

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/img/icon-not-found.svg
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/img/icon-not-found.svg b/new/ui-modules/blueprint-composer/app/img/icon-not-found.svg
new file mode 100644
index 0000000..b6f0d3d
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/img/icon-not-found.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!--
+  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.
+-->
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="510px" height="510px" viewBox="-150 -150 820 820" xml:space="preserve">
+<g>
+	<g id="do-not-disturb">
+		<path d="M255,0C114.75,0,0,114.75,0,255s114.75,255,255,255s255-114.75,255-255S395.25,0,255,0z M51,255c0-112.2,91.8-204,204-204
+			c45.9,0,89.25,15.3,124.95,43.35l-285.6,285.6C66.3,344.25,51,300.9,51,255z M255,459c-45.9,0-89.25-15.3-124.95-43.35
+			L415.65,130.05C443.7,165.75,459,209.1,459,255C459,367.2,367.2,459,255,459z" fill="#d9534f" />
+	</g>
+</g>
+</svg>

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/index.e2e-spec.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/index.e2e-spec.js b/new/ui-modules/blueprint-composer/app/index.e2e-spec.js
new file mode 100644
index 0000000..118c533
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/index.e2e-spec.js
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+const request = require('request');
+
+const APP_NAME = 'Blueprint composer';
+
+describe('App Shell', ()=> {
+    browser.get('./');
+    browser.waitForAngular();
+
+    describe('metadata', ()=> {
+        it('should have the correct title', ()=> {
+            expect(browser.getTitle()).toEqual('Brooklyn - ' + APP_NAME);
+        });
+    });
+});

http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/index.html
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/index.html b/new/ui-modules/blueprint-composer/app/index.html
new file mode 100644
index 0000000..0464f36
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/index.html
@@ -0,0 +1,37 @@
+<!--
+  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.
+-->
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8"/>
+        <meta http-equiv="x-ua-compatible" content="ie=edge"/>
+        <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"/>
+        <link rel="icon" type="image/png" href="${require('!!file-loader!<%= brand.product.favicon %>')}"/>
+        <title><%= getBrandedText('product.name') %> - <%= app.appname %></title>
+    </head>
+    <body ng-app="app" br-server-status ng-strict-di>
+        ${require('ejs-html!brooklyn-shared/partials/header.html')}
+
+        <main class="page-main-area" id="main-content" ui-view ng-cloak></main>
+
+        <br-interstitial-spinner event-count="1">
+            ${require('ejs-html!brooklyn-shared/partials/interstitial.html')}
+        </br-interstitial-spinner>
+    </body>
+</html>


Mime
View raw message