[CORD-1927] Fixing generate url for service models

Change-Id: I08d0d853ba2ab041626e133d079ab4562d73171e
diff --git a/src/app/datasources/helpers/model-discoverer.service.ts b/src/app/datasources/helpers/model-discoverer.service.ts
index c6dcb7e..0addc05 100644
--- a/src/app/datasources/helpers/model-discoverer.service.ts
+++ b/src/app/datasources/helpers/model-discoverer.service.ts
@@ -27,6 +27,7 @@
 import {IXosRuntimeStatesService, IXosState} from '../../core/services/runtime-states';
 import {IXosModelStoreService} from '../stores/model.store';
 import {IXosAuthService} from '../rest/auth.rest';
+import {IXosModeldefsCache} from './modeldefs.service';
 
 export interface IXosModel {
   name: string; // the model name
@@ -44,7 +45,6 @@
 // Service
 export interface IXosModelDiscovererService {
   discover(): ng.IPromise<boolean>;
-  get(modelName: string): IXosModel;
   getApiUrlFromModel(model: IXosModel): string;
   areModelsLoaded(): boolean;
 }
@@ -59,9 +59,10 @@
     'XosNavigationService',
     'XosModelStore',
     'ngProgressFactory',
-    'AuthService'
+    'AuthService',
+    'XosModeldefsCache'
   ];
-  private xosModels: IXosModel[] = []; // list of augmented model definitions;
+
   private xosServices: string[] = []; // list of loaded services
   private progressBar;
   private modelsLoaded: boolean = false;
@@ -75,7 +76,8 @@
     private XosNavigationService: IXosNavigationService,
     private XosModelStore: IXosModelStoreService,
     private ngProgressFactory: any, // check for type defs
-    private AuthService: IXosAuthService
+    private AuthService: IXosAuthService,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {
     this.progressBar = this.ngProgressFactory.createInstance();
     this.progressBar.setColor('#f6a821');
@@ -85,16 +87,12 @@
     return this.modelsLoaded;
   }
 
-  public get(modelName: string): IXosModel|null {
-    return _.find(this.xosModels, m => m.name === modelName);
-  }
-
   public getApiUrlFromModel(model: IXosModel): string {
     if (model.app === 'core') {
       return `/core/${this.ConfigHelpers.pluralize(model.name.toLowerCase())}`;
     }
     else {
-      const serviceName = this.serviceNameFromAppName(model.app);
+      const serviceName = this.XosModeldefsCache.serviceNameFromAppName(model.app);
       return `/${serviceName}/${this.ConfigHelpers.pluralize(model.name.toLowerCase())}`;
     }
   }
@@ -104,7 +102,7 @@
     this.progressBar.start();
     this.XosModelDefs.get()
       .then((modelsDef: IXosModeldef[]) => {
-
+        // TODO store modeldefs and add a method to retrieve the model definition from the name
         const pArray = [];
         _.forEach(modelsDef, (model: IXosModeldef) => {
           this.$log.debug(`[XosModelDiscovererService] Loading: ${model.name}`);
@@ -155,12 +153,8 @@
     return d.promise;
   }
 
-  private serviceNameFromAppName(appName: string): string {
-    return appName.replace('services.', '');
-  }
-
   private stateNameFromModel(model: IXosModel): string {
-    return `xos.${this.serviceNameFromAppName(model.app)}.${model.name.toLowerCase()}`;
+    return `xos.${this.XosModeldefsCache.serviceNameFromAppName(model.app)}.${model.name.toLowerCase()}`;
   }
 
   private getParentStateFromModel(model: IXosModel): string {
@@ -177,7 +171,7 @@
 
   // add a service state and navigation item if it is not already there
   private addService(model: IXosModel): string {
-    const serviceName: string = this.serviceNameFromAppName(model.app);
+    const serviceName: string = this.XosModeldefsCache.serviceNameFromAppName(model.app);
     if (!_.find(this.xosServices, n => n === serviceName)) {
       const serviceState = {
         url: serviceName,
@@ -222,7 +216,7 @@
       );
 
       // extend model
-      model.clientUrl = `${this.serviceNameFromAppName(model.app)}${clientUrl}`;
+      model.clientUrl = `${this.XosModeldefsCache.serviceNameFromAppName(model.app)}${clientUrl}`;
 
       d.resolve(model);
     } catch (e) {
@@ -300,9 +294,7 @@
 
     const d = this.$q.defer();
 
-    if (!_.find(this.xosModels, m => m.name === model.name)) {
-      this.xosModels.push(model);
-    }
+    this.XosModeldefsCache.cache(model);
 
     d.resolve(model);
 
diff --git a/src/app/datasources/helpers/model.discoverer.service.spec.ts b/src/app/datasources/helpers/model.discoverer.service.spec.ts
index 7a9e54f..8011442 100644
--- a/src/app/datasources/helpers/model.discoverer.service.spec.ts
+++ b/src/app/datasources/helpers/model.discoverer.service.spec.ts
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2017-present Open Networking Foundation
 
@@ -22,6 +21,7 @@
 import {XosModelDiscovererService, IXosModelDiscovererService} from './model-discoverer.service';
 import {IXosModeldef} from '../rest/modeldefs.rest';
 import {BehaviorSubject} from 'rxjs';
+import {XosModeldefsCache} from './modeldefs.service';
 
 const stubModels: IXosModeldef[] = [
   {
@@ -101,7 +101,8 @@
       .value('XosModelStore', MockXosModelStore)
       .value('ngProgressFactory', MockngProgressFactory)
       .value('XosNavigationService', MockXosNavigationService)
-      .value('AuthService', {});
+      .value('AuthService', {})
+      .service('XosModeldefsCache', XosModeldefsCache);
 
     angular.mock.module('test');
   });
@@ -152,21 +153,6 @@
     expect(service.getApiUrlFromModel(model)).toBe('/test/tenants');
   });
 
-  it('should retrieve a model definition from local cache', () => {
-    const model = {
-      name: 'Node',
-      app: 'core'
-    };
-    service['xosModels'] = [
-      model
-    ];
-    expect(service.get('Node')).toEqual(model);
-  });
-
-  it('should get the service name from the app name', () => {
-    expect(service['serviceNameFromAppName']('services.vsg')).toBe('vsg');
-  });
-
   it('should get the state name from the model', () => {
     expect(service['stateNameFromModel']({name: 'Tenant', app: 'services.vsg'})).toBe('xos.vsg.tenant');
   });
@@ -276,12 +262,6 @@
     );
   });
 
-  it('should store the model in memory', () => {
-    service['storeModel']({name: 'Tenant'});
-    expect(service['xosModels'][0]).toEqual({name: 'Tenant'});
-    expect(service['xosModels'].length).toEqual(1);
-  });
-
   describe('when discovering models', () => {
     beforeEach(() => {
       spyOn(service, 'cacheModelEntries').and.callThrough();
@@ -296,7 +276,7 @@
       service.discover()
         .then((res) => {
           expect(MockProgressBar.start).toHaveBeenCalled();
-          expect(MockXosModelDefs.get).toHaveBeenCalled();
+          expect(MockXosModelDefs.get).toHaveBeenCalled(); // FIXME replace correct spy
           expect(service['cacheModelEntries'].calls.count()).toBe(2);
           expect(service['addState'].calls.count()).toBe(2);
           expect(service['addNavItem'].calls.count()).toBe(2);
diff --git a/src/app/datasources/helpers/modeldefs.service.spec.ts b/src/app/datasources/helpers/modeldefs.service.spec.ts
new file mode 100644
index 0000000..9d5660d
--- /dev/null
+++ b/src/app/datasources/helpers/modeldefs.service.spec.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017-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 * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import {XosModeldefsCache, IXosModeldefsCache} from './modeldefs.service';
+import {IXosModel} from './model-discoverer.service';
+
+describe('The XosModeldefsCache service', () => {
+  let service;
+
+  beforeEach(() => {
+    angular
+      .module('test', [])
+      .service('XosModeldefsCache', XosModeldefsCache);
+
+    angular.mock.module('test');
+  });
+
+  beforeEach(angular.mock.inject((XosModeldefsCache: IXosModeldefsCache) => {
+    service = XosModeldefsCache;
+  }));
+
+  beforeEach(() => {
+    service['xosModelDefs'] = [];
+  });
+
+  describe('the cache method', () => {
+
+    const modelDef: IXosModel = {
+      fields: [
+        {name: 'id', type: 'number'},
+        {name: 'foo', type: 'string'}
+      ],
+      relations: [],
+      name: 'Node',
+      app: 'core',
+      description: '',
+      verbose_name: ''
+    };
+    it('should add a modelDefs', () => {
+      service.cache(modelDef);
+      expect(service['xosModelDefs'].length).toBe(1);
+    });
+
+    it('should not add a modelDefs twice', () => {
+      service.cache(modelDef);
+      service.cache(modelDef);
+      expect(service['xosModelDefs'].length).toBe(1);
+    });
+  });
+
+  it('should get the service name from the app name', () => {
+    expect(service.serviceNameFromAppName('services.vsg')).toBe('vsg');
+  });
+
+  describe('the get method', () => {
+    it('should retrieve a model definition from local cache', () => {
+      const model = {
+        name: 'Node',
+        app: 'core'
+      };
+      service['xosModelDefs'] = [
+        model
+      ];
+      expect(service.get('Node')).toEqual(model);
+    });
+  });
+});
diff --git a/src/app/datasources/helpers/modeldefs.service.ts b/src/app/datasources/helpers/modeldefs.service.ts
new file mode 100644
index 0000000..152b5e8
--- /dev/null
+++ b/src/app/datasources/helpers/modeldefs.service.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017-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 * as _ from 'lodash';
+import * as pluralize from 'pluralize';
+import {IXosModel} from './model-discoverer.service';
+
+export interface IXosModeldefsCache {
+  cache(modelDef: IXosModel): void;
+  get(modelName: string): IXosModel;
+  getApiUrlFromModel(model: IXosModel): string;
+  serviceNameFromAppName(appName: string): string;
+}
+
+export class XosModeldefsCache implements IXosModeldefsCache {
+
+  private xosModelDefs: IXosModel[] = [];
+
+  public cache(modelDef: IXosModel): void {
+    if (!_.find(this.xosModelDefs, m => m.name === modelDef.name)) {
+      this.xosModelDefs.push(modelDef);
+    }
+  }
+
+  public get(modelName: string): IXosModel|null {
+    return _.find(this.xosModelDefs, m => m.name === modelName);
+  }
+
+  public serviceNameFromAppName(appName: string): string {
+    return appName.replace('services.', '');
+  }
+
+  public getApiUrlFromModel(model: IXosModel): string {
+    // FIXME move pluralize from config to a separate service
+    if (model.app === 'core') {
+      return `/core/${pluralize(model.name.toLowerCase())}`;
+    }
+    else {
+      const serviceName = this.serviceNameFromAppName(model.app);
+      return `/${serviceName}/${pluralize(model.name.toLowerCase())}`;
+    }
+  }
+}
diff --git a/src/app/datasources/helpers/store.helpers.spec.ts b/src/app/datasources/helpers/store.helpers.spec.ts
index 5d0a584..f3b2321 100644
--- a/src/app/datasources/helpers/store.helpers.spec.ts
+++ b/src/app/datasources/helpers/store.helpers.spec.ts
@@ -25,11 +25,13 @@
 import {IWSEvent} from '../websocket/global';
 import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
 import {AuthService} from '../rest/auth.rest';
+import {IXosModeldefsCache} from './modeldefs.service';
 
 let service: IStoreHelpersService;
 let subject: BehaviorSubject<any>;
 let resource: ng.resource.IResourceClass<any>;
 let $resource: ng.resource.IResourceService;
+let xosModelCache: IXosModeldefsCache;
 
 describe('The StoreHelpers service', () => {
 
@@ -40,6 +42,10 @@
       .service('ModelRest', ModelRest) // NOTE evaluate mock
       .service('StoreHelpers', StoreHelpers)
       .service('AuthService', AuthService)
+      .value('XosModeldefsCache', {
+        get: jasmine.createSpy('XosModeldefsCache.get'),
+        getApiUrlFromModel: jasmine.createSpy('XosModeldefsCache.getApiUrlFromModel')
+      })
       .value('AppConfig', {});
 
     angular.mock.module('test');
@@ -47,10 +53,12 @@
 
   beforeEach(angular.mock.inject((
     StoreHelpers: IStoreHelpersService,
+    XosModeldefsCache: IXosModeldefsCache,
     _$resource_: ng.resource.IResourceService
   ) => {
     $resource = _$resource_;
     resource = $resource('/test');
+    xosModelCache = XosModeldefsCache;
     service = StoreHelpers;
   }));
 
@@ -58,13 +66,38 @@
     expect(service.updateCollection).toBeDefined();
   });
 
+  describe('the updateCollection method', () => {
 
-  it('should convert a core model name in an URL', () => {
-    expect(service.urlFromCoreModel('Slice')).toBe('/core/slices');
-    expect(service.urlFromCoreModel('Xos')).toBe('/core/xoses');
-    expect(service.urlFromCoreModel('SiteRole')).toBe('/core/siteroles');
-    expect(service.urlFromCoreModel('SliceRole')).toBe('/core/sliceroles');
-    expect(service.urlFromCoreModel('SlicePrivilege')).toBe('/core/sliceprivileges');
+    beforeEach(() => {
+      subject = new BehaviorSubject([
+        new resource({id: 1, name: 'test'})
+      ]);
+
+    });
+
+    it('should get the correct url for a core model', () => {
+      const mockModelDef = {
+        name: 'Node',
+        app: 'core'
+      };
+
+      xosModelCache.get['and'].returnValue(mockModelDef);
+
+      const event: IWSEvent = {
+        model: 'TestModel',
+        msg: {
+          object: {
+            id: 1,
+            name: 'test'
+          },
+          changed_fields: ['deleted']
+        }
+      };
+
+      service.updateCollection(event, subject);
+      expect(xosModelCache.get).toHaveBeenCalledWith('TestModel');
+      expect(xosModelCache.getApiUrlFromModel).toHaveBeenCalledWith(mockModelDef);
+    });
   });
 
   describe('when updating a collection', () => {
diff --git a/src/app/datasources/helpers/store.helpers.ts b/src/app/datasources/helpers/store.helpers.ts
index 83c3b9a..0df37c7 100644
--- a/src/app/datasources/helpers/store.helpers.ts
+++ b/src/app/datasources/helpers/store.helpers.ts
@@ -18,27 +18,26 @@
 
 import {BehaviorSubject} from 'rxjs';
 import * as _ from 'lodash';
-import * as pluralize from 'pluralize';
 import {IWSEvent} from '../websocket/global';
 import {IXosResourceService} from '../rest/model.rest';
+import {IXosModeldefsCache} from './modeldefs.service';
 
 export interface IStoreHelpersService {
-  urlFromCoreModel(name: string): string;
   updateCollection(event: IWSEvent, subject: BehaviorSubject<any>): BehaviorSubject<any>;
 }
 
 export class StoreHelpers implements IStoreHelpersService {
-  static $inject = ['ModelRest'];
+  static $inject = [
+    'ModelRest',
+    'XosModeldefsCache'
+  ];
 
   constructor (
-    private modelRest: IXosResourceService
+    private modelRest: IXosResourceService,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {
   }
 
-  public urlFromCoreModel(name: string): string {
-    return `/core/${pluralize(name.toLowerCase())}`;
-  }
-
   public updateCollection(event: IWSEvent, subject: BehaviorSubject<any>): BehaviorSubject<any> {
     const collection: any[] = subject.value;
     const index: number = _.findIndex(collection, (i) => {
@@ -49,7 +48,8 @@
     const isDeleted: boolean = _.includes(event.msg.changed_fields, 'deleted');
 
     // generate a resource for the model
-    const endpoint = this.urlFromCoreModel(event.model);
+    const modelDef = this.XosModeldefsCache.get(event.model); // get the model definition
+    const endpoint = this.XosModeldefsCache.getApiUrlFromModel(modelDef);
     const resource = this.modelRest.getResource(endpoint);
     const model = new resource(event.msg.object);
 
diff --git a/src/app/datasources/index.ts b/src/app/datasources/index.ts
index e5adcab..d5869b5 100644
--- a/src/app/datasources/index.ts
+++ b/src/app/datasources/index.ts
@@ -26,6 +26,7 @@
 import {xosCore} from '../core/index';
 import {SearchService} from './helpers/search.service';
 import {XosModelDiscovererService} from './helpers/model-discoverer.service';
+import {XosModeldefsCache} from './helpers/modeldefs.service';
 
 export const xosDataSources = 'xosDataSources';
 
@@ -34,6 +35,7 @@
   .service('ModelRest', ModelRest)
   .service('AuthService', AuthService)
   .service('WebSocket', WebSocketEvent)
+  .service('XosModeldefsCache', XosModeldefsCache)
   .service('StoreHelpers', StoreHelpers)
   .service('SynchronizerStore', SynchronizerStore)
   .service('XosModelStore', XosModelStore)
diff --git a/src/app/datasources/stores/model.store.spec.ts b/src/app/datasources/stores/model.store.spec.ts
index 4e94495..09dc3f9 100644
--- a/src/app/datasources/stores/model.store.spec.ts
+++ b/src/app/datasources/stores/model.store.spec.ts
@@ -27,11 +27,13 @@
 import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
 import {AuthService} from '../rest/auth.rest';
 import {XosDebouncer} from '../../core/services/helpers/debounce.helper';
+import {IXosModeldefsCache} from '../helpers/modeldefs.service';
 
 let service: IXosModelStoreService;
 let httpBackend: ng.IHttpBackendService;
 let $scope;
 let WebSocket;
+let XosModeldefsCache;
 
 class MockWs {
   private _list;
@@ -69,6 +71,10 @@
       .service('ConfigHelpers', ConfigHelpers) // TODO mock
       .service('AuthService', AuthService)
       .constant('AppConfig', MockAppCfg)
+      .value('XosModeldefsCache', {
+        get: jasmine.createSpy('XosModeldefsCache.get').and.returnValue({}),
+        getApiUrlFromModel: jasmine.createSpy('XosModeldefsCache.getApiUrlFromModel')
+      })
       .service('XosDebouncer', XosDebouncer);
 
     angular.mock.module('ModelStore');
@@ -78,12 +84,14 @@
     XosModelStore: IXosModelStoreService,
     $httpBackend: ng.IHttpBackendService,
     _$rootScope_: ng.IRootScopeService,
-    _WebSocket_: any
+    _WebSocket_: any,
+    _XosModeldefsCache_: IXosModeldefsCache
   ) => {
     service = XosModelStore;
     httpBackend = $httpBackend;
     $scope = _$rootScope_;
     WebSocket = _WebSocket_;
+    XosModeldefsCache = _XosModeldefsCache_;
 
     // ModelRest will call the backend
     httpBackend.whenGET(`${MockAppCfg.apiEndpoint}/core/samples`)
@@ -96,6 +104,7 @@
     });
 
     it('the first event should be the resource response', (done) => {
+      XosModeldefsCache.getApiUrlFromModel.and.returnValue(`/core/samples`);
       let event = 0;
       service.query('sample')
         .subscribe(collection => {
@@ -114,6 +123,7 @@
   describe('the GET method', () => {
     it('should return an observable containing a single model', (done) => {
       let event = 0;
+      XosModeldefsCache.getApiUrlFromModel.and.returnValue(`/core/samples`);
       service.get('sample', queryData[1].id)
         .subscribe((model) => {
           event++;
@@ -177,6 +187,8 @@
     it('should create different Subject', (done) => {
       let fevent = 0;
       let sevent = 0;
+      XosModeldefsCache.get.and.callFake(v => v);
+      XosModeldefsCache.getApiUrlFromModel.and.callFake(v => `/core/${v}s`);
       service.query('first')
         .subscribe(first => {
           fevent++;
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index 7735ff0..10c4d2e 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -23,6 +23,7 @@
 import {IXosResourceService} from '../rest/model.rest';
 import {IStoreHelpersService} from '../helpers/store.helpers';
 import {IXosDebouncer} from '../../core/services/helpers/debounce.helper';
+import {IXosModeldefsCache} from '../helpers/modeldefs.service';
 
 export interface  IXosModelStoreService {
   query(model: string, apiUrl?: string): Observable<any>;
@@ -31,7 +32,14 @@
 }
 
 export class XosModelStore implements IXosModelStoreService {
-  static $inject = ['$log', 'WebSocket', 'StoreHelpers', 'ModelRest', 'XosDebouncer'];
+  static $inject = [
+    '$log',
+    'WebSocket',
+    'StoreHelpers',
+    'ModelRest',
+    'XosDebouncer',
+    'XosModeldefsCache'
+  ];
   private _collections: any; // NOTE contains a map of {model: BehaviourSubject}
   private efficientNext: any; // NOTE debounce next
   constructor(
@@ -39,7 +47,8 @@
     private webSocket: IWSEventService,
     private storeHelpers: IStoreHelpersService,
     private ModelRest: IXosResourceService,
-    private XosDebouncer: IXosDebouncer
+    private XosDebouncer: IXosDebouncer,
+    private XosModeldefsCache: IXosModeldefsCache
   ) {
     this._collections = {};
     this.efficientNext = this.XosDebouncer.debounce(this.next, 500, this, false);
@@ -59,6 +68,7 @@
       this.efficientNext(this._collections[modelName]);
     }
 
+    // NOTE do we need to subscriber every time we query?
     this.webSocket.list()
       .filter((e: IWSEvent) => e.model === modelName)
       .subscribe(
@@ -128,8 +138,7 @@
   private loadInitialData(model: string, apiUrl?: string) {
     // TODO provide always the apiUrl together with the query() params
     if (!angular.isDefined(apiUrl)) {
-      // NOTE check what is the correct pattern to pluralize this
-      apiUrl = this.storeHelpers.urlFromCoreModel(model);
+      apiUrl = this.XosModeldefsCache.getApiUrlFromModel(this.XosModeldefsCache.get(model));
     }
     this.ModelRest.getResource(apiUrl).query().$promise
       .then(
diff --git a/src/app/datasources/websocket/global.ts b/src/app/datasources/websocket/global.ts
index 32f6f4a..e86282a 100644
--- a/src/app/datasources/websocket/global.ts
+++ b/src/app/datasources/websocket/global.ts
@@ -57,7 +57,6 @@
 
         if (data.msg.changed_fields.length === 0 || _.intersection(data.msg.changed_fields, ignoredFields).length === data.msg.changed_fields.length) {
           // NOTE means that the only updated fields does not change anything in the UI, so don't send events around
-          this.$log.debug(`[WebSocket] Ignoring Event for: ${data.model} [${data.msg.pk}]`);
           return;
         }