GUI2 Framework as a standalone NPM Library

There are a few changes going on here
1) The fw part of GUI has been moved out in to its own project
 a) several files are renamed (files 21-83)
 b) the project has its own BUILD file (file 5)
 c) there are a few files created by Angular CLI here - mostly script generated (files 7-20)
 d) package-lock.json is a big generated file that has to be versioned (file 13)

2) The view in the main GUI2 project now refer to this library (see BUILD file 110)
 a) some useless files were removed (files 115 - 139)
 b) several files are changed to update references (files 140-202)
 c) this breaks the BUCK build so I've removed the BUCK file and references to it (file 109)

Change-Id: I48bc3253edfcf5947f1582731ba739a1296012f5
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.spec.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.spec.ts
new file mode 100644
index 0000000..5b86d56
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.spec.ts
@@ -0,0 +1,492 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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 { TestBed, inject } from '@angular/core/testing';
+
+import { LogService } from '../log.service';
+import { ConsoleLoggerService } from '../consolelogger.service';
+import { FnService } from './fn.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import * as d3 from 'd3';
+
+class MockActivatedRoute extends ActivatedRoute {
+    constructor(params: Params) {
+        super();
+        this.queryParams = of(params);
+    }
+}
+
+/**
+ * ONOS GUI -- Util -- General Purpose Functions - Unit Tests
+ */
+describe('FnService', () => {
+    let ar: ActivatedRoute;
+    let fs: FnService;
+    let mockWindow: Window;
+    let logServiceSpy: jasmine.SpyObj<LogService>;
+
+    const someFunction = () => {};
+    const someArray = [1, 2, 3];
+    const someObject = { foo: 'bar'};
+    const someNumber = 42;
+    const someString = 'xyyzy';
+    const someDate = new Date();
+    const stringArray = ['foo', 'bar'];
+
+    beforeEach(() => {
+        const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn']);
+        ar = new MockActivatedRoute({'debug': 'TestService'});
+        mockWindow = <any>{
+            innerWidth: 400,
+            innerHeight: 200,
+            navigator: {
+                userAgent: 'defaultUA'
+            }
+        };
+
+
+        TestBed.configureTestingModule({
+            providers: [FnService,
+                { provide: LogService, useValue: logSpy },
+                { provide: ActivatedRoute, useValue: ar },
+                { provide: 'Window', useFactory: (() => mockWindow ) }
+            ]
+        });
+
+        fs = TestBed.get(FnService);
+        logServiceSpy = TestBed.get(LogService);
+    });
+
+    it('should be created', () => {
+        expect(fs).toBeTruthy();
+    });
+
+    // === Tests for isF()
+    it('isF(): null for undefined', () => {
+        expect(fs.isF(undefined)).toBeNull();
+    });
+
+    it('isF(): null for null', () => {
+        expect(fs.isF(null)).toBeNull();
+    });
+    it('isF(): the reference for function', () => {
+        expect(fs.isF(someFunction)).toBe(someFunction);
+    });
+    it('isF(): null for string', () => {
+        expect(fs.isF(someString)).toBeNull();
+    });
+    it('isF(): null for number', () => {
+        expect(fs.isF(someNumber)).toBeNull();
+    });
+    it('isF(): null for Date', () => {
+        expect(fs.isF(someDate)).toBeNull();
+    });
+    it('isF(): null for array', () => {
+        expect(fs.isF(someArray)).toBeNull();
+    });
+    it('isF(): null for object', () => {
+        expect(fs.isF(someObject)).toBeNull();
+    });
+
+    // === Tests for isA()
+    it('isA(): null for undefined', () => {
+        expect(fs.isA(undefined)).toBeNull();
+    });
+    it('isA(): null for null', () => {
+        expect(fs.isA(null)).toBeNull();
+    });
+    it('isA(): null for function', () => {
+        expect(fs.isA(someFunction)).toBeNull();
+    });
+    it('isA(): null for string', () => {
+        expect(fs.isA(someString)).toBeNull();
+    });
+    it('isA(): null for number', () => {
+        expect(fs.isA(someNumber)).toBeNull();
+    });
+    it('isA(): null for Date', () => {
+        expect(fs.isA(someDate)).toBeNull();
+    });
+    it('isA(): the reference for array', () => {
+        expect(fs.isA(someArray)).toBe(someArray);
+    });
+    it('isA(): null for object', () => {
+        expect(fs.isA(someObject)).toBeNull();
+    });
+
+    // === Tests for isS()
+    it('isS(): null for undefined', () => {
+        expect(fs.isS(undefined)).toBeNull();
+    });
+    it('isS(): null for null', () => {
+        expect(fs.isS(null)).toBeNull();
+    });
+    it('isS(): null for function', () => {
+        expect(fs.isS(someFunction)).toBeNull();
+    });
+    it('isS(): the reference for string', () => {
+        expect(fs.isS(someString)).toBe(someString);
+    });
+    it('isS(): null for number', () => {
+        expect(fs.isS(someNumber)).toBeNull();
+    });
+    it('isS(): null for Date', () => {
+        expect(fs.isS(someDate)).toBeNull();
+    });
+    it('isS(): null for array', () => {
+        expect(fs.isS(someArray)).toBeNull();
+    });
+    it('isS(): null for object', () => {
+        expect(fs.isS(someObject)).toBeNull();
+    });
+
+    // === Tests for isO()
+    it('isO(): null for undefined', () => {
+        expect(fs.isO(undefined)).toBeNull();
+    });
+    it('isO(): null for null', () => {
+        expect(fs.isO(null)).toBeNull();
+    });
+    it('isO(): null for function', () => {
+        expect(fs.isO(someFunction)).toBeNull();
+    });
+    it('isO(): null for string', () => {
+        expect(fs.isO(someString)).toBeNull();
+    });
+    it('isO(): null for number', () => {
+        expect(fs.isO(someNumber)).toBeNull();
+    });
+    it('isO(): null for Date', () => {
+        expect(fs.isO(someDate)).toBeNull();
+    });
+    it('isO(): null for array', () => {
+        expect(fs.isO(someArray)).toBeNull();
+    });
+    it('isO(): the reference for object', () => {
+        expect(fs.isO(someObject)).toBe(someObject);
+    });
+
+
+    // === Tests for contains()
+    it('contains(): false for non-array', () => {
+        expect(fs.contains(null, 1)).toBeFalsy();
+    });
+    it('contains(): true for contained item', () => {
+        expect(fs.contains(someArray, 1)).toBeTruthy();
+        expect(fs.contains(stringArray, 'bar')).toBeTruthy();
+    });
+    it('contains(): false for non-contained item', () => {
+        expect(fs.contains(someArray, 109)).toBeFalsy();
+        expect(fs.contains(stringArray, 'zonko')).toBeFalsy();
+    });
+
+    // === Tests for areFunctions()
+    it('areFunctions(): true for empty-array', () => {
+        expect(fs.areFunctions({}, [])).toBeTruthy();
+    });
+    it('areFunctions(): true for some api', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: () => {}
+        }, ['b', 'a'])).toBeTruthy();
+    });
+    it('areFunctions(): false for some other api', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: 'not-a-function'
+        }, ['b', 'a'])).toBeFalsy();
+    });
+    it('areFunctions(): extraneous stuff NOT ignored', () => {
+        expect(fs.areFunctions({
+            a: () => {},
+            b: () => {},
+            c: 1,
+            d: 'foo'
+        }, ['a', 'b'])).toBeFalsy();
+    });
+    it('areFunctions(): extraneous stuff ignored (alternate fn)', () => {
+        expect(fs.areFunctionsNonStrict({
+            a: () => {},
+            b: () => {},
+            c: 1,
+            d: 'foo'
+        }, ['a', 'b'])).toBeTruthy();
+    });
+
+    // == use the now-tested areFunctions() on our own api:
+    it('should define api functions', () => {
+        expect(fs.areFunctions(fs, [
+            'isF', 'isA', 'isS', 'isO', 'contains',
+            'areFunctions', 'areFunctionsNonStrict', 'windowSize',
+            'isMobile', 'isChrome', 'isChromeHeadless', 'isSafari',
+            'isFirefox', 'parseDebugFlags',
+            'debugOn', 'debug', 'find', 'inArray', 'removeFromArray',
+            'isEmptyObject', 'cap', 'noPx', 'noPxStyle', 'endsWith',
+            'inEvilList', 'analyze', 'sanitize', 'sameObjProps', 'containsObj'
+//            'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
+//            'eecode', 'noPx', 'noPxStyle', 'endsWith', 'addToTrie', 'removeFromTrie', 'trieLookup',
+//            'classNames', 'extend', 'sanitize'
+        ])).toBeTruthy();
+    });
+
+
+    // === Tests for windowSize()
+    it('windowSize(): adjust height', () => {
+        const dim = fs.windowSize(50);
+        expect(dim.width).toEqual(400);
+        expect(dim.height).toEqual(150);
+    });
+
+    it('windowSize(): adjust width', () => {
+        const dim = fs.windowSize(0, 50);
+        expect(dim.width).toEqual(350);
+        expect(dim.height).toEqual(200);
+    });
+
+    it('windowSize(): adjust width and height', () => {
+        const dim = fs.windowSize(101, 201);
+        expect(dim.width).toEqual(199);
+        expect(dim.height).toEqual(99);
+    });
+
+    // === Tests for isMobile()
+    const uaMap = {
+        chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) ' +
+                'AppleWebKit/537.36 (KHTML, like Gecko) ' +
+                'Chrome/41.0.2272.89 Safari/537.36',
+
+        iPad: 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) ' +
+                'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+                'Mobile/11A465 Safari/9537.53',
+
+        iPhone: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) ' +
+                'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+                'Mobile/11A465 Safari/9537.53'
+    };
+
+    function setUa(key) {
+        const str = uaMap[key];
+        expect(str).toBeTruthy();
+        (<any>mockWindow.navigator).userAgent = str;
+    }
+
+    it('isMobile(): should be false for Chrome on Mac OS X', () => {
+        setUa('chrome');
+        expect(fs.isMobile()).toBe(false);
+    });
+    it('isMobile(): should be true for Safari on iPad', () => {
+        setUa('iPad');
+        expect(fs.isMobile()).toBe(true);
+    });
+    it('isMobile(): should be true for Safari on iPhone', () => {
+        setUa('iPhone');
+        expect(fs.isMobile()).toBe(true);
+    });
+
+    // === Tests for find()
+    const dataset = [
+        { id: 'foo', name: 'Furby'},
+        { id: 'bar', name: 'Barbi'},
+        { id: 'baz', name: 'Basil'},
+        { id: 'goo', name: 'Gabby'},
+        { id: 'zoo', name: 'Zevvv'}
+    ];
+
+    it('should not find ooo', () => {
+        expect(fs.find('ooo', dataset)).toEqual(-1);
+    });
+    it('should find foo', () => {
+        expect(fs.find('foo', dataset)).toEqual(0);
+    });
+    it('should find zoo', () => {
+        expect(fs.find('zoo', dataset)).toEqual(4);
+    });
+
+    it('should not find Simon', () => {
+        expect(fs.find('Simon', dataset, 'name')).toEqual(-1);
+    });
+    it('should find Furby', () => {
+        expect(fs.find('Furby', dataset, 'name')).toEqual(0);
+    });
+    it('should find Zevvv', () => {
+        expect(fs.find('Zevvv', dataset, 'name')).toEqual(4);
+    });
+
+
+    // === Tests for inArray()
+    const objRef = { x: 1, y: 2 };
+    const array = [1, 3.14, 'hey', objRef, 'there', true];
+    const array2 = ['b', 'a', 'd', 'a', 's', 's'];
+
+    it('should not find HOO', () => {
+        expect(fs.inArray('HOO', array)).toEqual(-1);
+    });
+    it('should find 1', () => {
+        expect(fs.inArray(1, array)).toEqual(0);
+    });
+    it('should find pi', () => {
+        expect(fs.inArray(3.14, array)).toEqual(1);
+    });
+    it('should find hey', () => {
+        expect(fs.inArray('hey', array)).toEqual(2);
+    });
+    it('should find the object', () => {
+        expect(fs.inArray(objRef, array)).toEqual(3);
+    });
+    it('should find there', () => {
+        expect(fs.inArray('there', array)).toEqual(4);
+    });
+    it('should find true', () => {
+        expect(fs.inArray(true, array)).toEqual(5);
+    });
+
+    it('should find the first occurrence A', () => {
+        expect(fs.inArray('a', array2)).toEqual(1);
+    });
+    it('should find the first occurrence S', () => {
+        expect(fs.inArray('s', array2)).toEqual(4);
+    });
+    it('should not find X', () => {
+        expect(fs.inArray('x', array2)).toEqual(-1);
+    });
+
+    // === Tests for removeFromArray()
+    it('should keep the array the same, for non-match', () => {
+        const array1 = [1, 2, 3];
+        expect(fs.removeFromArray(4, array1)).toBe(false);
+        expect(array1).toEqual([1, 2, 3]);
+    });
+    it('should remove a value', () => {
+        const array1a = [1, 2, 3];
+        expect(fs.removeFromArray(2, array1a)).toBe(true);
+        expect(array1a).toEqual([1, 3]);
+    });
+    it('should remove the first occurrence', () => {
+        const array1b = ['x', 'y', 'z', 'z', 'y'];
+        expect(fs.removeFromArray('y', array1b)).toBe(true);
+        expect(array1b).toEqual(['x', 'z', 'z', 'y']);
+        expect(fs.removeFromArray('x', array1b)).toBe(true);
+        expect(array1b).toEqual(['z', 'z', 'y']);
+    });
+
+    // === Tests for isEmptyObject()
+    it('should return true if an object is empty', () => {
+        expect(fs.isEmptyObject({})).toBe(true);
+    });
+    it('should return false if an object is not empty', () => {
+        expect(fs.isEmptyObject({foo: 'bar'})).toBe(false);
+    });
+
+    // === Tests for cap()
+    it('should ignore non-alpha', () => {
+        expect(fs.cap('123')).toEqual('123');
+    });
+    it('should capitalize first char', () => {
+        expect(fs.cap('Foo')).toEqual('Foo');
+        expect(fs.cap('foo')).toEqual('Foo');
+        expect(fs.cap('foo bar')).toEqual('Foo bar');
+        expect(fs.cap('FOO BAR')).toEqual('Foo bar');
+        expect(fs.cap('foo Bar')).toEqual('Foo bar');
+    });
+
+    // === Tests for noPx()
+    it('should return the value without px suffix', () => {
+        expect(fs.noPx('10px')).toBe(10);
+        expect(fs.noPx('500px')).toBe(500);
+        expect(fs.noPx('-80px')).toBe(-80);
+    });
+
+    // === Tests for noPxStyle()
+    it('should give a style\'s property without px suffix', () => {
+        const d3Elem = d3.select('body')
+            .append('div')
+            .attr('id', 'fooElem')
+            .style('width', '500px')
+            .style('height', '200px')
+            .style('font-size', '12px');
+        expect(fs.noPxStyle(d3Elem, 'width')).toBe(500);
+        expect(fs.noPxStyle(d3Elem, 'height')).toBe(200);
+        expect(fs.noPxStyle(d3Elem, 'font-size')).toBe(12);
+        d3.select('#fooElem').remove();
+    });
+
+    // === Tests for endsWith()
+    it('should return true if string ends with foo', () => {
+        expect(fs.endsWith('barfoo', 'foo')).toBe(true);
+    });
+
+    it('should return false if string doesnt end with foo', () => {
+        expect(fs.endsWith('barfood', 'foo')).toBe(false);
+    });
+
+    // === Tests for sanitize()
+    it('should return foo', () => {
+        expect(fs.sanitize('foo')).toEqual('foo');
+    });
+    it('should retain < b > tags', () => {
+        const str = 'foo <b>bar</b> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < i > tags', () => {
+        const str = 'foo <i>bar</i> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < p > tags', () => {
+        const str = 'foo <p>bar</p> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < em > tags', () => {
+        const str = 'foo <em>bar</em> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+    it('should retain < strong > tags', () => {
+        const str = 'foo <strong>bar</strong> baz';
+        expect(fs.sanitize(str)).toEqual(str);
+    });
+
+    it('should reject < a > tags', () => {
+        expect(fs.sanitize('test <a href="hah">something</a> this'))
+            .toEqual('test something this');
+    });
+
+    it('should log a warning for < script > tags', () => {
+        expect(fs.sanitize('<script>alert("foo");</script>'))
+            .toEqual('');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <script> detected!'
+        );
+    });
+    it('should log a warning for < style > tags', () => {
+        expect(fs.sanitize('<style> h1 {color:red;} </style>'))
+            .toEqual('');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <style> detected!'
+        );
+    });
+
+    it('should log a warning for < iframe > tags', () => {
+        expect(fs.sanitize('Foo<iframe><body><h1>fake</h1></body></iframe>Bar'))
+            .toEqual('FooBar');
+        expect(logServiceSpy.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <iframe> detected!'
+        );
+    });
+
+    it('should completely strip < script >, remove < a >, retain < i >', () => {
+        expect(fs.sanitize('Hey <i>this</i> is <script>alert("foo");</script> <a href="meh">cool</a>'))
+            .toEqual('Hey <i>this</i> is  cool');
+    });
+});
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.ts
new file mode 100644
index 0000000..506ce74
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/fn.service.ts
@@ -0,0 +1,564 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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 { Injectable, Inject } from '@angular/core';
+import { ActivatedRoute, Router} from '@angular/router';
+import { LogService } from '../log.service';
+
+// Angular>=2 workaround for missing definition
+declare const InstallTrigger: any;
+
+const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
+const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
+const evillist: string[] = ['script', 'style', 'iframe'];
+
+/**
+ * Used with the Window size function;
+ **/
+export interface WindowSize {
+    width: number;
+    height: number;
+}
+
+/**
+ * For the sanitize() and analyze() functions
+ */
+export interface Match {
+    full: string;
+    name: string;
+}
+
+// TODO Move all this trie stuff to its own class
+// Angular>=2 Tightened up on types to avoid compiler errors
+interface TrieC {
+    p: any;
+    s: string[];
+}
+// trie operation
+function _trieOp(op: string, trie, word: string, data) {
+    const p = trie;
+    const w: string = word.toUpperCase();
+    const s: Array<string> = w.split('');
+    let c: TrieC = { p: p, s: s };
+    let t = [];
+    let  x = 0;
+    const f1 = op === '+' ? add : probe;
+    const f2 = op === '+' ? insert : remove;
+
+    function add(cAdded): TrieC {
+        const q = cAdded.s.shift();
+        let np = cAdded.p[q];
+
+        if (!np) {
+            cAdded.p[q] = {};
+            np = cAdded.p[q];
+            x = 1;
+        }
+        return { p: np, s: cAdded.s };
+    }
+
+    function probe(cProbed): TrieC {
+        const q = cProbed.s.shift();
+        const k: number = Object.keys(cProbed.p).length;
+        const np = cProbed.p[q];
+
+        t.push({ q: q, k: k, p: cProbed.p });
+        if (!np) {
+            t = [];
+            return { p: [], s: [] };
+        }
+        return { p: np, s: cProbed.s };
+    }
+
+    function insert() {
+        c.p._data = data;
+        return x ? 'added' : 'updated';
+    }
+
+    function remove() {
+        if (t.length) {
+            t = t.reverse();
+            while (t.length) {
+                const d = t.shift();
+                delete d.p[d.q];
+                if (d.k > 1) {
+                    t = [];
+                }
+            }
+            return 'removed';
+        }
+        return 'absent';
+    }
+
+    while (c.s.length) {
+        c = f1(c);
+    }
+    return f2();
+}
+
+// add word to trie (word will be converted to uppercase)
+// data associated with the word
+// returns 'added' or 'updated'
+function addToTrie(trie, word, data) {
+    return _trieOp('+', trie, word, data);
+}
+
+// remove word from trie (word will be converted to uppercase)
+// returns 'removed' or 'absent'
+// Angular>=2 added in quotes for data. error TS2554: Expected 4 arguments, but got 3.
+function removeFromTrie(trie, word) {
+    return _trieOp('-', trie, word, '');
+}
+
+// lookup word (converted to uppercase) in trie
+// returns:
+//    undefined if the word is not in the trie
+//    -1 for a partial match (word is a prefix to an existing word)
+//    data for the word for an exact match
+function trieLookup(trie, word) {
+    const s = word.toUpperCase().split('');
+    let p = trie;
+    let n;
+
+    while (s.length) {
+        n = s.shift();
+        p = p[n];
+        if (!p) {
+            return undefined;
+        }
+    }
+    if (p._data) {
+        return p._data;
+    }
+    return -1;
+}
+
+
+/**
+ * ONOS GUI -- Util -- General Purpose Functions
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class FnService {
+    // internal state
+    private debugFlags = new Map<string, boolean>([
+//        [ "LoadingService", true ]
+    ]);
+
+    constructor(
+        private route: ActivatedRoute,
+        private log: LogService,
+        // TODO: Change the any type to Window when https://github.com/angular/angular/issues/15640 is fixed.
+        @Inject('Window') private w: any
+    ) {
+        this.route.queryParams.subscribe(params => {
+            const debugparam: string = params['debug'];
+//            log.debug('Param:', debugparam);
+            this.parseDebugFlags(debugparam);
+        });
+//        this.log.debug('FnService constructed');
+    }
+
+    /**
+     * Test if an argument is a function
+     *
+     * Note: the need for this would go away if all functions
+     * were strongly typed
+     */
+    isF(f: any): any {
+        return typeof f === 'function' ? f : null;
+    }
+
+    /**
+     * Test if an argument is an array
+     *
+     * Note: the need for this would go away if all arrays
+     * were strongly typed
+     */
+    isA(a: any): any {
+    // NOTE: Array.isArray() is part of EMCAScript 5.1
+        return Array.isArray(a) ? a : null;
+    }
+
+    /**
+     * Test if an argument is a string
+     *
+     * Note: the need for this would go away if all strings
+     * were strongly typed
+     */
+    isS(s: any): string {
+        return typeof s === 'string' ? s : null;
+    }
+
+    /**
+     * Test if an argument is an object
+     *
+     * Note: the need for this would go away if all objects
+     * were strongly typed
+     */
+    isO(o: any): Object {
+        return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
+    }
+
+    /**
+     * Test that an array contains an object
+     */
+    contains(a: any[], x: any): boolean {
+        return this.isA(a) && a.indexOf(x) > -1;
+    }
+
+    /**
+     * Returns width and height of window inner dimensions.
+     * offH, offW : offset width/height are subtracted, if present
+     */
+    windowSize(offH: number = 0, offW: number = 0): WindowSize {
+        return {
+            height: this.w.innerHeight - offH,
+            width: this.w.innerWidth - offW
+        };
+    }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise.
+     * Also returns false if there are properties on the api that are NOT
+     * listed in the array of names.
+     *
+     * This gets extra complicated when the api Object is an
+     * Angular service - while the functions can be retrieved
+     * by an indexed get, the ownProperties does not show the
+     * functions of the class. We have to dive in to the prototypes
+     * properties to get these - and even then we have to filter
+     * out the constructor and any member variables
+     */
+    areFunctions(api: Object, fnNames: string[]): boolean {
+        const fnLookup: Map<string, boolean> = new Map();
+        let extraFound: boolean = false;
+
+        if (!this.isA(fnNames)) {
+            return false;
+        }
+
+        const n: number = fnNames.length;
+        let i: number;
+        let name: string;
+
+        for (i = 0; i < n; i++) {
+            name = fnNames[i];
+            if (!this.isF(api[name])) {
+                return false;
+            }
+            fnLookup.set(name, true);
+        }
+
+        // check for properties on the API that are not listed in the array,
+        const keys = Object.getOwnPropertyNames(api);
+        if (keys.length === 0) {
+            return true;
+        }
+        // If the api is a class it will have a name,
+        //  else it will just be called 'Object'
+        const apiObjectName: string = api.constructor.name;
+        if (apiObjectName === 'Object') {
+            Object.keys(api).forEach((key) => {
+                if (!fnLookup.get(key)) {
+                    extraFound = true;
+                }
+            });
+        } else { // It is a class, so its functions will be in the child (prototype)
+            const pObj: Object = Object.getPrototypeOf(api);
+            for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
+                if (key === 'constructor') { // Filter out constructor
+                    continue;
+                }
+                const value = Object.getOwnPropertyDescriptor(pObj, key);
+                // Only compare functions. Look for any not given in the map
+                if (this.isF(value.value) && !fnLookup.get(key)) {
+                    extraFound = true;
+                }
+            }
+        }
+        return !extraFound;
+    }
+
+    /**
+     * Returns true if all names in the array are defined as functions
+     * on the given api object; false otherwise. This is a non-strict version
+     * that does not care about other properties on the api.
+     */
+    areFunctionsNonStrict(api, fnNames): boolean {
+        if (!this.isA(fnNames)) {
+            return false;
+        }
+        const n = fnNames.length;
+        let i;
+        let name;
+
+        for (i = 0; i < n; i++) {
+            name = fnNames[i];
+            if (!this.isF(api[name])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if current browser determined to be a mobile device
+     */
+    isMobile() {
+        const ua = this.w.navigator.userAgent;
+        const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
+        return patt.test(ua);
+    }
+
+    /**
+     * Returns true if the current browser determined to be Chrome
+     */
+    isChrome() {
+        const isChromium = (this.w as any).chrome;
+        const vendorName = this.w.navigator.vendor;
+
+        const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
+        return (isChromium !== null &&
+        isChromium !== undefined &&
+        vendorName === 'Google Inc.' &&
+        isOpera === false);
+    }
+
+    isChromeHeadless() {
+        const vendorName = this.w.navigator.vendor;
+        const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
+
+        return (vendorName === 'Google Inc.' && headlessChrome === true);
+    }
+
+    /**
+     * Returns true if the current browser determined to be Safari
+     */
+    isSafari() {
+        return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
+        this.w.navigator.userAgent.indexOf('Chrome') === -1);
+    }
+
+    /**
+     * Returns true if the current browser determined to be Firefox
+     */
+    isFirefox() {
+        return typeof InstallTrigger !== 'undefined';
+    }
+
+    /**
+     * search through an array of objects, looking for the one with the
+     * tagged property matching the given key. tag defaults to 'id'.
+     * returns the index of the matching object, or -1 for no match.
+     */
+    find(key: string, array: Object[], tag: string = 'id'): number {
+        let idx: number;
+        const n: number = array.length;
+
+        for (idx = 0 ; idx < n; idx++) {
+            const d: Object = array[idx];
+            if (d[tag] === key) {
+                return idx;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * search through array to find (the first occurrence of) item,
+     * returning its index if found; otherwise returning -1.
+     */
+    inArray(item: any, array: any[]): number {
+        if (this.isA(array)) {
+            for (let i = 0; i < array.length; i++) {
+                if (array[i] === item) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * remove (the first occurrence of) the specified item from the given
+     * array, if any. Return true if the removal was made; false otherwise.
+     */
+    removeFromArray(item: any, array: any[]): boolean {
+        const i: number = this.inArray(item, array);
+        if (i >= 0) {
+            array.splice(i, 1);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * return true if the object is empty, return false otherwise
+     */
+    isEmptyObject(obj: Object): boolean {
+        for (const key in obj) {
+            if (true) { return false; }
+        }
+        return true;
+    }
+
+    /**
+     * returns true if the two objects have all the same properties
+     */
+    sameObjProps(obj1: Object, obj2: Object): boolean {
+        for (const key in obj1) {
+            if (obj1.hasOwnProperty(key)) {
+                if (!(obj1[key] === obj2[key])) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * returns true if the array contains the object
+     * does NOT use strict object reference equality,
+     * instead checks each property individually for equality
+     */
+    containsObj(arr: any[], obj: Object): boolean {
+        const len = arr.length;
+        for (let i = 0; i < len; i++) {
+            if (this.sameObjProps(arr[i], obj)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the given string with the first character capitalized.
+     */
+    cap(s: string): string {
+        return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
+    }
+
+    /**
+     * return the parameter without a px suffix
+     */
+    noPx(num: string): number {
+        return Number(num.replace(/px$/, ''));
+    }
+
+    /**
+     * return an element's given style property without px suffix
+     */
+    noPxStyle(elem: any, prop: string): number {
+        return Number(elem.style(prop).replace(/px$/, ''));
+    }
+
+    /**
+     * Return true if a str ends with suffix
+     */
+    endsWith(str: string, suffix: string) {
+        return str.indexOf(suffix, str.length - suffix.length) !== -1;
+    }
+
+    /**
+     * output debug message to console, if debug tag set...
+     * e.g. fs.debug('mytag', arg1, arg2, ...)
+     */
+    debug(tag, ...args) {
+        if (this.debugFlags.get(tag)) {
+//            this.log.debug(tag, args.join());
+        }
+    }
+
+    private parseDebugFlags(dbgstr: string): void {
+        const bits = dbgstr ? dbgstr.split(',') : [];
+        bits.forEach((key) => {
+            this.debugFlags.set(key, true);
+        });
+//        this.log.debug('Debug flags:', dbgstr);
+    }
+
+    /**
+      * Return true if the given debug flag was specified in the query params
+      */
+    debugOn(tag: string): boolean {
+        return this.debugFlags.get(tag);
+    }
+
+
+
+    // -----------------------------------------------------------------
+    // The next section deals with sanitizing external strings destined
+    // to be loaded via a .html() function call.
+    //
+    // See definition of matcher, evillist and whitelist at the top of this file
+
+    /*
+     * Returns true if the tag is in the evil list, (and is not an end-tag)
+     */
+    inEvilList(tag: any): boolean {
+        return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+    }
+
+    /*
+     * Returns an array of Matches of matcher in html
+     */
+    analyze(html: string): Match[] {
+        const matches: Match[] = [];
+        let match;
+
+        // extract all tags
+        while ((match = matcher.exec(html)) !== null) {
+            matches.push({
+                full: match[0],
+                name: match[1],
+                // NOTE: ignoring attributes {match[2].split(' ')} for now
+            });
+        }
+
+        return matches;
+    }
+
+    /*
+     * Returns a cleaned version of html
+     */
+    sanitize(html: string): string {
+        const matches: Match[] = this.analyze(html);
+
+        // completely obliterate evil tags and their contents...
+        evillist.forEach((tag) => {
+            const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
+            html = html.replace(re, '');
+        });
+
+        // filter out all but white-listed tags and end-tags
+        matches.forEach((tag) => {
+            if (whitelist.indexOf(tag.name) === -1) {
+                html = html.replace(tag.full, '');
+                if (this.inEvilList(tag)) {
+                    this.log.warn('Unsanitary HTML input -- ' +
+                        tag.full + ' detected!');
+                }
+            }
+        });
+
+        // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
+
+        return html;
+    }
+
+}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/lion.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/lion.service.ts
new file mode 100644
index 0000000..b8cde73
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/lion.service.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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 { Injectable } from '@angular/core';
+import { LogService } from '../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+/**
+ * A definition of Lion data
+ */
+export interface Lion {
+    locale: any;
+    lion: any;
+}
+
+/**
+ * ONOS GUI -- Lion -- Localization Utilities
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class LionService {
+
+    ubercache: any[] = [];
+    loadCbs = new Map<string, () => void>([]); // A map of functions
+
+    /**
+     * Handler for uberlion event from WSS
+     */
+    uberlion(data: Lion) {
+        this.ubercache = data.lion;
+
+        this.log.info('LION service: Locale... [' + data.locale + ']');
+        this.log.info('LION service: Bundles installed...');
+
+        for (const p in this.ubercache) {
+            if (this.ubercache[p]) {
+                this.log.info('            :=> ', p);
+            }
+        }
+        // If any component had registered a callback, call it now
+        // that LION is loaded
+        for (const cbname of this.loadCbs.keys()) {
+            this.log.debug('Updating', cbname, 'with LION');
+            this.loadCbs.get(cbname)();
+        }
+
+        this.log.debug('LION service: uber-lion bundle received:', data);
+    }
+
+    constructor(
+        private log: LogService,
+        private wss: WebSocketService
+    ) {
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            ['uberlion', (data) => this.uberlion(data) ]
+        ]));
+        this.log.debug('LionService constructed');
+    }
+
+    /**
+     * Returns a lion bundle (function) for the given bundle ID (string)
+     * returns a function that takes a string and returns a string
+     */
+    bundle(bundleId: string): (string) => string {
+        let bundleObj = this.ubercache[bundleId];
+
+        if (!bundleObj) {
+            this.log.warn('No lion bundle registered:', bundleId);
+            bundleObj = {};
+        }
+
+        return (key) =>  {
+            return bundleObj[key] || '%' + key + '%';
+        };
+    }
+}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/prefs.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/prefs.service.ts
new file mode 100644
index 0000000..8145921
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/prefs.service.ts
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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 { Injectable } from '@angular/core';
+import { FnService } from './fn.service';
+import { LogService } from '../log.service';
+import { WebSocketService } from '../remote/websocket.service';
+
+/**
+ * ONOS GUI -- Util -- User Preference Service
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class PrefsService {
+    protected Prefs;
+    protected handlers: string[] = [];
+    cache: any;
+    listeners: any;
+    constructor(
+        protected fs: FnService,
+        protected log: LogService,
+        protected wss: WebSocketService
+    ) {
+        this.cache = {};
+        this.wss.bindHandlers(new Map<string, (data) => void>([
+            [this.Prefs, (data) => this.updatePrefs(data)]
+        ]));
+        this.handlers.push(this.Prefs);
+
+        this.log.debug('PrefsService constructed');
+    }
+
+    setPrefs(name: string, obj: any) {
+        // keep a cached copy of the object and send an update to server
+        this.cache[name] = obj;
+        this.wss.sendEvent('updatePrefReq', { key: name, value: obj });
+    }
+    updatePrefs(data: any) {
+        this.cache = data;
+        this.listeners.forEach(function (lsnr) { lsnr(); });
+    }
+
+    asNumbers(obj: any, keys?: any, not?: any) {
+        if (!obj) {
+            return null;
+        }
+
+        const skip = {};
+        if (not) {
+            keys.forEach(k => {
+                skip[k] = 1;
+            }
+            );
+        }
+
+        if (!keys || not) {
+            // do them all
+            Array.from(obj).forEach((v, k) => {
+                if (!not || !skip[k]) {
+                    obj[k] = Number(obj[k]);
+                }
+            });
+        } else {
+            // do the explicitly named keys
+            keys.forEach(k => {
+                obj[k] = Number(obj[k]);
+            });
+        }
+        return obj;
+    }
+
+    getPrefs(name: string, defaults: any, qparams?: string) {
+        const obj = Object.assign({}, defaults || {}, this.cache[name] || {});
+
+        // if query params are specified, they override...
+        if (this.fs.isO(qparams)) {
+            obj.forEach(k => {
+                if (qparams.hasOwnProperty(k)) {
+                    obj[k] = qparams[k];
+                }
+            });
+        }
+        return obj;
+    }
+
+    // merge preferences:
+    // The assumption here is that obj is a sparse object, and that the
+    //  defined keys should overwrite the corresponding values, but any
+    //  existing keys that are NOT explicitly defined here should be left
+    //  alone (not deleted).
+    mergePrefs(name: string, obj: any) {
+        const merged = this.cache[name] || {};
+        this.setPrefs(name, Object.assign(merged, obj));
+    }
+
+    addListener(listener: any) {
+        this.listeners.push(listener);
+    }
+
+    removeListener(listener: any) {
+        this.listeners = this.listeners.filter(function (obj) { return obj === listener; });
+    }
+
+}
diff --git a/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/theme.service.ts b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/theme.service.ts
new file mode 100644
index 0000000..993d9f3
--- /dev/null
+++ b/web/gui2-fw-lib/projects/gui2-fw-lib/src/lib/util/theme.service.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed 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 { Injectable } from '@angular/core';
+import { LogService } from '../log.service';
+
+
+/**
+ * ONOS GUI -- Util -- Theme Service
+ */
+@Injectable({
+  providedIn: 'root',
+})
+export class ThemeService {
+    themes: string[] = ['light', 'dark'];
+    thidx = 0;
+
+    constructor(
+        private log: LogService
+    ) {
+        this.log.debug('ThemeService constructed');
+    }
+
+    getTheme(): string {
+        return this.themes[this.thidx];
+    }
+
+    themeStr(): string {
+        return this.themes.join(' ');
+    }
+
+}