From a233856bacd2a49d512b7d00ff053cf2cfd76364 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 17 Sep 2015 13:15:42 -0700 Subject: [PATCH 1/7] [Core] Separate out contextualize Separate out contextualize function to facilitate reuse from entanglement bundle. nasa/openmctweb#84 --- platform/core/bundle.json | 7 +- .../src/capabilities/CompositionCapability.js | 16 ++-- .../src/capabilities/ContextCapability.js | 2 +- platform/core/src/services/Contextualize.js | 78 +++++++++++++++++++ 4 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 platform/core/src/services/Contextualize.js diff --git a/platform/core/bundle.json b/platform/core/bundle.json index 4b33f48f35..74d85c3c25 100644 --- a/platform/core/bundle.json +++ b/platform/core/bundle.json @@ -149,7 +149,7 @@ { "key": "composition", "implementation": "capabilities/CompositionCapability.js", - "depends": [ "$injector" ] + "depends": [ "$injector", "contextualize" ] }, { "key": "relationship", @@ -204,6 +204,11 @@ { "key": "topic", "implementation": "services/Topic.js" + }, + { + "key": "contextualize", + "implementation": "services/Contextualize.js", + "depends": [ "$log" ] } ], "roots": [ diff --git a/platform/core/src/capabilities/CompositionCapability.js b/platform/core/src/capabilities/CompositionCapability.js index f1b2532040..4204eddd39 100644 --- a/platform/core/src/capabilities/CompositionCapability.js +++ b/platform/core/src/capabilities/CompositionCapability.js @@ -25,8 +25,7 @@ * Module defining CompositionCapability. Created by vwoeltje on 11/7/14. */ define( - ["./ContextualDomainObject"], - function (ContextualDomainObject) { + function () { "use strict"; /** @@ -41,12 +40,13 @@ define( * @constructor * @implements {Capability} */ - function CompositionCapability($injector, domainObject) { + function CompositionCapability($injector, contextualize, domainObject) { // Get a reference to the object service from $injector this.injectObjectService = function () { this.objectService = $injector.get("objectService"); }; + this.contextualize = contextualize; this.domainObject = domainObject; } @@ -58,19 +58,17 @@ define( CompositionCapability.prototype.invoke = function () { var domainObject = this.domainObject, model = domainObject.getModel(), + contextualize = this.contextualize, ids; // Then filter out non-existent objects, // and wrap others (such that they expose a // "context" capability) - function contextualize(objects) { + function contextualizeObjects(objects) { return ids.filter(function (id) { return objects[id]; }).map(function (id) { - return new ContextualDomainObject( - objects[id], - domainObject - ); + return contextualize(objects[id], domainObject); }); } @@ -86,7 +84,7 @@ define( this.lastModified = model.modified; // Load from the underlying object service this.lastPromise = this.objectService.getObjects(ids) - .then(contextualize); + .then(contextualizeObjects); } return this.lastPromise; diff --git a/platform/core/src/capabilities/ContextCapability.js b/platform/core/src/capabilities/ContextCapability.js index 9ffaf4a5bb..39770f9b5b 100644 --- a/platform/core/src/capabilities/ContextCapability.js +++ b/platform/core/src/capabilities/ContextCapability.js @@ -84,7 +84,7 @@ define( parentContext = parentObject && parentObject.getCapability('context'), parentPath = parentContext ? - parentContext.getPath() : [ this.parentObject ]; + parentContext.getPath() : [ this.parentObject ]; return parentPath.concat([this.domainObject]); }; diff --git a/platform/core/src/services/Contextualize.js b/platform/core/src/services/Contextualize.js new file mode 100644 index 0000000000..d231557369 --- /dev/null +++ b/platform/core/src/services/Contextualize.js @@ -0,0 +1,78 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define( + ['../capabilities/ContextualDomainObject'], + function (ContextualDomainObject) { + "use strict"; + + /** + * Wrap a domain object such that it has a `context` capability + * referring to a specific parent. + * + * Usage: + * + * contextualize(domainObject, parentObject) + * + * Attempting to contextualize an object with a parent that does + * not include that object in its composition may have + * unpredictable results; a warning will be logged if this occurs. + * + * @returns {Function} + * @memberof platform/core + */ + function Contextualize($log) { + function validate(id, parentObject) { + var model = parentObject && parentObject.getModel(), + composition = (model || {}).composition || []; + if (composition.indexOf(id) === -1) { + $log.warn([ + "Attempted to contextualize", + id, + "in", + parentObject && parentObject.getId(), + "but that object does not contain", + id, + "in its composition.", + "Unexpected behavior may follow." + ].join(" ")); + } + } + + /** + * Contextualize this domain object. + * @param {DomainObject} domainObject the domain object + * to wrap with a context + * @param {DomainObject} parentObject the domain object + * which should appear as the contextual parent + */ + return function (domainObject, parentObject) { + validate(domainObject.getId(), parentObject); + return new ContextualDomainObject(domainObject, parentObject); + }; + } + + return Contextualize; + } +); + From d1c0d81120cd145dad583b054150b991fd5ac377 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 17 Sep 2015 13:37:57 -0700 Subject: [PATCH 2/7] [Entanglement] Contextualize objects by location Add a context capability to domain objects based on their location at the time they are loaded. nasa/openmctweb#84 --- platform/entanglement/bundle.json | 6 ++ .../src/services/LocatingObjectDecorator.js | 88 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 platform/entanglement/src/services/LocatingObjectDecorator.js diff --git a/platform/entanglement/bundle.json b/platform/entanglement/bundle.json index f85c5cdf4c..61c3d90539 100644 --- a/platform/entanglement/bundle.json +++ b/platform/entanglement/bundle.json @@ -37,6 +37,12 @@ "type": "decorator", "provides": "creationService", "implementation": "services/LocatingCreationDecorator.js" + }, + { + "type": "decorator", + "provides": "objectService", + "implementation": "services/LocatingObjectDecorator.js", + "depends": ["contextualize", "$q", "$log"] } ], "controllers": [ diff --git a/platform/entanglement/src/services/LocatingObjectDecorator.js b/platform/entanglement/src/services/LocatingObjectDecorator.js new file mode 100644 index 0000000000..1993c9cc94 --- /dev/null +++ b/platform/entanglement/src/services/LocatingObjectDecorator.js @@ -0,0 +1,88 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ + +define( + function () { + "use strict"; + + /** + * Ensures that domain objects are loaded with a context capability + * that reflects their location. + * @constructor + * @implements {ObjectService} + * @memberof platform/entanglement + */ + function LocatingObjectDecorator(contextualize, $q, $log, objectService) { + this.contextualize = contextualize; + this.$log = $log; + this.objectService = objectService; + this.$q = $q; + } + + LocatingObjectDecorator.prototype.getObjects = function (ids) { + var $q = this.$q, + $log = this.$log, + contextualize = this.contextualize, + objectService = this.objectService, + result = {}; + + function loadObjectInContext(id, exclude) { + function attachContextById(domainObject, locationId) { + return loadObjectInContext(locationId, exclude) + .then(function (parent) { + return contextualize(domainObject, parent); + }); + } + + function attachContextForLocation(domainObject) { + var model = domainObject && domainObject.getModel(), + location = (model || {}).location; + + // Don't pursue a context if we encounter this + // object again during this sequence of invocations. + exclude[id] = true; + + return location ? + attachContextById(domainObject, location) : + domainObject; + } + + return objectService.getObjects([id]).then(function (objects) { + return exclude[id] ? + objects[id] : // Don't loop indefinitely. + attachContextForLocation(objects[id]); + }); + } + + ids.forEach(function (id) { + result[id] = loadObjectInContext(id, {}); + }); + + return $q.all(result); + }; + + return LocatingObjectDecorator; + } +); + From 88b8528aafbec2fadb80d71f7e010e6e9d5510fd Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 17 Sep 2015 13:42:08 -0700 Subject: [PATCH 3/7] [Entanglement] Log a warning on cycle detection nasa/openmctweb#84 --- .../src/services/LocatingObjectDecorator.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/platform/entanglement/src/services/LocatingObjectDecorator.js b/platform/entanglement/src/services/LocatingObjectDecorator.js index 1993c9cc94..6fde642ee0 100644 --- a/platform/entanglement/src/services/LocatingObjectDecorator.js +++ b/platform/entanglement/src/services/LocatingObjectDecorator.js @@ -69,9 +69,18 @@ define( } return objectService.getObjects([id]).then(function (objects) { - return exclude[id] ? - objects[id] : // Don't loop indefinitely. - attachContextForLocation(objects[id]); + if (exclude[id]) { + $log.warn([ + "LocatingObjectDecorator detected a cycle", + "while attempted to define a context for", + id + ";", + "no context will be added and unexpected behavior", + "may follow." + ].join(" ")); + return objects[id]; + } + + return attachContextForLocation(objects[id]); }); } From c4aed5716521ab199003a1b31fe1604f6c2f3c28 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 17 Sep 2015 13:51:47 -0700 Subject: [PATCH 4/7] [Entanglement] Refactor LocatingObjectDecorator Rearrange code in LocatingObjectDecorator to make it easier to follow; add comments. --- .../src/services/LocatingObjectDecorator.js | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/platform/entanglement/src/services/LocatingObjectDecorator.js b/platform/entanglement/src/services/LocatingObjectDecorator.js index 6fde642ee0..f4be5f1900 100644 --- a/platform/entanglement/src/services/LocatingObjectDecorator.js +++ b/platform/entanglement/src/services/LocatingObjectDecorator.js @@ -47,28 +47,19 @@ define( objectService = this.objectService, result = {}; + // Load a single object using location to establish a context function loadObjectInContext(id, exclude) { - function attachContextById(domainObject, locationId) { - return loadObjectInContext(locationId, exclude) - .then(function (parent) { - return contextualize(domainObject, parent); - }); - } - - function attachContextForLocation(domainObject) { - var model = domainObject && domainObject.getModel(), + function attachContext(objects) { + var domainObject = (objects || {})[id], + model = domainObject && domainObject.getModel(), location = (model || {}).location; - // Don't pursue a context if we encounter this - // object again during this sequence of invocations. - exclude[id] = true; + // If no location is defined, we can't look up a context. + if (!location) { + return domainObject; + } - return location ? - attachContextById(domainObject, location) : - domainObject; - } - - return objectService.getObjects([id]).then(function (objects) { + // Avoid looping indefinitely on cyclical locations if (exclude[id]) { $log.warn([ "LocatingObjectDecorator detected a cycle", @@ -77,11 +68,21 @@ define( "no context will be added and unexpected behavior", "may follow." ].join(" ")); - return objects[id]; + return domainObject; } - return attachContextForLocation(objects[id]); - }); + // Record that we've visited this ID to detect cycles. + exclude[id] = true; + + // Do the recursive step to get the parent... + return loadObjectInContext(location, exclude) + .then(function (parent) { + // ...and then contextualize with it! + return contextualize(domainObject, parent); + }); + } + + return objectService.getObjects([id]).then(attachContext); } ids.forEach(function (id) { From bc4c7feb6c019993abc16adf0e71beb1fae69d40 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 17 Sep 2015 13:57:20 -0700 Subject: [PATCH 5/7] [Core] Update spec for CompositionCapability ...to reflect changes to separate out contextualization of domain objects --- .../capabilities/CompositionCapabilitySpec.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/platform/core/test/capabilities/CompositionCapabilitySpec.js b/platform/core/test/capabilities/CompositionCapabilitySpec.js index 518cb2e795..c6f6b76aba 100644 --- a/platform/core/test/capabilities/CompositionCapabilitySpec.js +++ b/platform/core/test/capabilities/CompositionCapabilitySpec.js @@ -25,8 +25,11 @@ * CompositionCapabilitySpec. Created by vwoeltje on 11/6/14. */ define( - ["../../src/capabilities/CompositionCapability"], - function (CompositionCapability) { + [ + "../../src/capabilities/CompositionCapability", + "../../src/capabilities/ContextualDomainObject" + ], + function (CompositionCapability, ContextualDomainObject) { "use strict"; var DOMAIN_OBJECT_METHODS = [ @@ -40,6 +43,7 @@ define( describe("The composition capability", function () { var mockDomainObject, mockInjector, + mockContextualize, mockObjectService, composition; @@ -70,11 +74,19 @@ define( return (name === "objectService") && mockObjectService; } }; + mockContextualize = jasmine.createSpy('contextualize'); + + // Provide a minimal (e.g. no error-checking) implementation + // of contextualize for simplicity + mockContextualize.andCallFake(function (domainObject, parentObject) { + return new ContextualDomainObject(domainObject, parentObject); + }); mockObjectService.getObjects.andReturn(mockPromise([])); composition = new CompositionCapability( mockInjector, + mockContextualize, mockDomainObject ); }); @@ -113,4 +125,4 @@ define( }); } -); \ No newline at end of file +); From d92ae4d508e2ee7f5274b35e57189a5e9454a856 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 17 Sep 2015 14:06:22 -0700 Subject: [PATCH 6/7] [Core] Add tests for 'contextualize' --- .../core/test/services/ContextualizeSpec.js | 90 +++++++++++++++++++ platform/core/test/suite.json | 1 + 2 files changed, 91 insertions(+) create mode 100644 platform/core/test/services/ContextualizeSpec.js diff --git a/platform/core/test/services/ContextualizeSpec.js b/platform/core/test/services/ContextualizeSpec.js new file mode 100644 index 0000000000..7acefcc600 --- /dev/null +++ b/platform/core/test/services/ContextualizeSpec.js @@ -0,0 +1,90 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../../src/services/Contextualize"], + function (Contextualize) { + "use strict"; + + var DOMAIN_OBJECT_METHODS = [ + 'getId', + 'getModel', + 'getCapability', + 'hasCapability', + 'useCapability' + ]; + + describe("The 'contextualize' service", function () { + var mockLog, + mockDomainObject, + mockParentObject, + testParentModel, + contextualize; + + beforeEach(function () { + testParentModel = { composition: ["abc"] }; + + mockLog = jasmine.createSpyObj( + "$log", + [ "error", "warn", "info", "debug" ] + ); + + mockDomainObject = + jasmine.createSpyObj('domainObject', DOMAIN_OBJECT_METHODS); + mockParentObject = + jasmine.createSpyObj('parentObject', DOMAIN_OBJECT_METHODS); + + mockDomainObject.getId.andReturn("abc"); + mockDomainObject.getModel.andReturn({}); + mockParentObject.getId.andReturn("parent"); + mockParentObject.getModel.andReturn(testParentModel); + + contextualize = new Contextualize(mockLog); + }); + + it("attaches a context capability", function () { + var contextualizedObject = + contextualize(mockDomainObject, mockParentObject); + + expect(contextualizedObject.getId()).toEqual("abc"); + expect(contextualizedObject.getCapability("context")) + .toBeDefined(); + expect(contextualizedObject.getCapability("context").getParent()) + .toBe(mockParentObject); + }); + + it("issues a warning if composition does not match", function () { + // Precondition - normally it should not issue a warning + contextualize(mockDomainObject, mockParentObject); + expect(mockLog.warn).not.toHaveBeenCalled(); + + testParentModel.composition = ["xyz"]; + + contextualize(mockDomainObject, mockParentObject); + expect(mockLog.warn).toHaveBeenCalled(); + }); + + + }); + } +); diff --git a/platform/core/test/suite.json b/platform/core/test/suite.json index e2a7d8f57a..012a1b3503 100644 --- a/platform/core/test/suite.json +++ b/platform/core/test/suite.json @@ -24,6 +24,7 @@ "objects/DomainObject", "objects/DomainObjectProvider", + "services/Contextualize", "services/Now", "services/Throttle", "services/Topic", From 411f0d904d4cb476a697371420fbb4c9e1fdc902 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 17 Sep 2015 14:45:05 -0700 Subject: [PATCH 7/7] [Entanglement] Test LocatingObjectDecorator --- .../services/LocatingObjectDecoratorSpec.js | 135 ++++++++++++++++++ platform/entanglement/test/suite.json | 1 + 2 files changed, 136 insertions(+) create mode 100644 platform/entanglement/test/services/LocatingObjectDecoratorSpec.js diff --git a/platform/entanglement/test/services/LocatingObjectDecoratorSpec.js b/platform/entanglement/test/services/LocatingObjectDecoratorSpec.js new file mode 100644 index 0000000000..5946f982ff --- /dev/null +++ b/platform/entanglement/test/services/LocatingObjectDecoratorSpec.js @@ -0,0 +1,135 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ + +define( + [ + '../../src/services/LocatingObjectDecorator' + ], + function (LocatingObjectDecorator) { + "use strict"; + + describe("LocatingObjectDecorator", function () { + var mockContextualize, + mockQ, + mockLog, + mockObjectService, + mockCallback, + testObjects, + testModels, + decorator; + + function testPromise(v) { + return (v || {}).then ? v : { + then: function (callback) { + return testPromise(callback(v)); + } + }; + } + + beforeEach(function () { + // A <- B <- C + // D <-> E, to verify cycle detection + testModels = { + a: { name: "A" }, + b: { name: "B", location: "a" }, + c: { name: "C", location: "b" }, + d: { name: "D", location: "e" }, + e: { name: "E", location: "d" } + }; + testObjects = {}; + + mockContextualize = jasmine.createSpy("contextualize"); + mockQ = jasmine.createSpyObj("$q", ["when", "all"]); + mockLog = + jasmine.createSpyObj("$log", ["error", "warn", "info", "debug"]); + mockObjectService = + jasmine.createSpyObj("objectService", ["getObjects"]); + + mockContextualize.andCallFake(function (domainObject, parentObject) { + // Not really what contextualize does, but easy to test! + return { + testObject: domainObject, + testParent: parentObject + }; + }); + + mockQ.when.andCallFake(testPromise); + mockQ.all.andCallFake(function (promises) { + var result = {}; + Object.keys(promises).forEach(function (k) { + promises[k].then(function (v) { result[k] = v; }); + }); + return testPromise(result); + }); + + mockObjectService.getObjects.andReturn(testPromise(testObjects)); + + mockCallback = jasmine.createSpy("callback"); + + Object.keys(testModels).forEach(function (id) { + testObjects[id] = jasmine.createSpyObj( + "domainObject-" + id, + [ "getId", "getModel", "getCapability" ] + ); + testObjects[id].getId.andReturn(id); + testObjects[id].getModel.andReturn(testModels[id]); + }); + + decorator = new LocatingObjectDecorator( + mockContextualize, + mockQ, + mockLog, + mockObjectService + ); + }); + + it("contextualizes domain objects by location", function () { + decorator.getObjects(['b', 'c']).then(mockCallback); + expect(mockCallback).toHaveBeenCalledWith({ + b: { + testObject: testObjects.b, + testParent: testObjects.a + }, + c: { + testObject: testObjects.c, + testParent: { + testObject: testObjects.b, + testParent: testObjects.a + } + } + }); + }); + + it("warns on cycle detection", function () { + // Base case, no cycle, no warning + decorator.getObjects(['a', 'b', 'c']); + expect(mockLog.warn).not.toHaveBeenCalled(); + + decorator.getObjects(['e']); + expect(mockLog.warn).toHaveBeenCalled(); + }); + + }); + } +); diff --git a/platform/entanglement/test/suite.json b/platform/entanglement/test/suite.json index 9dfceb5c0f..12831b407a 100644 --- a/platform/entanglement/test/suite.json +++ b/platform/entanglement/test/suite.json @@ -5,5 +5,6 @@ "services/MoveService", "services/LocationService", "services/LocatingCreationDecorator", + "services/LocatingObjectDecorator", "capabilities/LocationCapability" ]