diff --git a/bundles.json b/bundles.json index f87f035720..94dd50da8d 100644 --- a/bundles.json +++ b/bundles.json @@ -24,6 +24,7 @@ "platform/features/events", "platform/forms", "platform/identity", + "platform/persistence/aggregator", "platform/persistence/local", "platform/persistence/queue", "platform/policy", diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 4c8760679c..b7c999bc20 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -677,6 +677,40 @@ If the provided capability has no invoke method, the return value here functions as `getCapability` including returning `undefined` if the capability is not exposed. +### Identifier Syntax + +For most purposes, a domain object identifier can be treated as a purely +symbolic string; these are typically generated by Open MCT Web and plug-ins +should rarely be concerned with its internal structure. + +A domain object identifier has one or two parts, separated by a colon. + +* If two parts are present, the part before the colon refers to the space + in which the domain object resides. This may be a persistence space or + a purely symbolic space recognized by a specific model provider. The + part after the colon is the key to use when looking up the domain object + model within that space. +* If only one part is present, the domain object has no space specified, + and may presume to reside in the application-configured default space + defined by the `PERSISTENCE_SPACE` constant. +* Both the key and the space identifier may consist of any combination + of alphanumeric characters, underscores, dashes, and periods. + +Some examples: + +* A domain object with the identifier `foo:xyz` would have its model + loaded using key `xyz` from persistence space `foo`. +* A domain object with the identifier `bar` would have its model loaded + using key `bar` from the space identified by the `PERSISTENCE_SPACE` + constant. + +```bnf + ::= ":" | + ::= + + ::= + + ::= | | "-" | "." | "_" +``` + ## Domain Object Actions An `Action` is behavior that can be performed upon/using a `DomainObject`. An @@ -2377,6 +2411,11 @@ default paths to reach external services are all correct. ### Configuration Constants +The following constants have global significance: +* `PERSISTENCE_SPACE`: The space in which domain objects should be persisted + (or read from) when not otherwise specified. Typically this will not need + to be overridden by other bundles, but persistence adapters may wish to + consume this constant in order to provide persistence for that space. The following configuration constants are recognized by Open MCT Web bundles: * Common UI elements - `platform/commonUI/general` diff --git a/example/scratchpad/README.md b/example/scratchpad/README.md new file mode 100644 index 0000000000..624a4786b4 --- /dev/null +++ b/example/scratchpad/README.md @@ -0,0 +1,2 @@ +Example of using multiple persistence stores by exposing a root +object with a different space prefix. diff --git a/example/scratchpad/bundle.json b/example/scratchpad/bundle.json new file mode 100644 index 0000000000..f95b467fd0 --- /dev/null +++ b/example/scratchpad/bundle.json @@ -0,0 +1,23 @@ +{ + "extensions": { + "roots": [ + { + "id": "scratch:root", + "model": { + "type": "folder", + "composition": [], + "name": "Scratchpad" + }, + "priority": "preferred" + } + ], + "components": [ + { + "provides": "persistenceService", + "type": "provider", + "implementation": "ScratchPersistenceProvider.js", + "depends": [ "$q" ] + } + ] + } +} diff --git a/example/scratchpad/src/ScratchPersistenceProvider.js b/example/scratchpad/src/ScratchPersistenceProvider.js new file mode 100644 index 0000000000..b45da31e5f --- /dev/null +++ b/example/scratchpad/src/ScratchPersistenceProvider.js @@ -0,0 +1,79 @@ +/***************************************************************************** + * 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,window*/ + +define( + [], + function () { + 'use strict'; + + /** + * The ScratchPersistenceProvider keeps JSON documents in memory + * and provides a persistence interface, but changes are lost on reload. + * @memberof example/scratchpad + * @constructor + * @implements {PersistenceService} + * @param q Angular's $q, for promises + */ + function ScratchPersistenceProvider($q) { + this.$q = $q; + this.table = {}; + } + + ScratchPersistenceProvider.prototype.listSpaces = function () { + return this.$q.when(['scratch']); + }; + + ScratchPersistenceProvider.prototype.listObjects = function (space) { + return this.$q.when( + space === 'scratch' ? Object.keys(this.table) : [] + ); + }; + + ScratchPersistenceProvider.prototype.createObject = function (space, key, value) { + if (space === 'scratch') { + this.table[key] = JSON.stringify(value); + } + return this.$q.when(space === 'scratch'); + }; + + ScratchPersistenceProvider.prototype.readObject = function (space, key) { + return this.$q.when( + (space === 'scratch' && this.table[key]) ? + JSON.parse(this.table[key]) : undefined + ); + }; + + ScratchPersistenceProvider.prototype.deleteObject = function (space, key, value) { + if (space === 'scratch') { + delete this.table[key]; + } + return this.$q.when(space === 'scratch'); + }; + + ScratchPersistenceProvider.prototype.updateObject = + ScratchPersistenceProvider.prototype.createObject; + + return ScratchPersistenceProvider; + } +); diff --git a/platform/core/bundle.json b/platform/core/bundle.json index 911678d5f4..b22e225e84 100644 --- a/platform/core/bundle.json +++ b/platform/core/bundle.json @@ -72,8 +72,7 @@ "persistenceService", "$q", "now", - "PERSISTENCE_SPACE", - "ADDITIONAL_PERSISTENCE_SPACES" + "PERSISTENCE_SPACE" ] }, { @@ -115,6 +114,12 @@ "type": "provider", "implementation": "views/ViewProvider.js", "depends": [ "views[]", "$log" ] + }, + { + "provides": "identifierService", + "type": "provider", + "implementation": "identifiers/IdentifierProvider.js", + "depends": [ "PERSISTENCE_SPACE" ] } ], "types": [ @@ -183,7 +188,7 @@ { "key": "persistence", "implementation": "capabilities/PersistenceCapability.js", - "depends": [ "persistenceService", "PERSISTENCE_SPACE" ] + "depends": [ "persistenceService", "identifierService" ] }, { "key": "metadata", @@ -202,7 +207,7 @@ { "key": "instantiation", "implementation": "capabilities/InstantiationCapability.js", - "depends": [ "$injector" ] + "depends": [ "$injector", "identifierService" ] } ], "services": [ @@ -245,11 +250,6 @@ { "key": "PERSISTENCE_SPACE", "value": "mct" - }, - { - "key": "ADDITIONAL_PERSISTENCE_SPACES", - "value": [], - "description": "An array of additional persistence spaces to load models from." } ], "licenses": [ diff --git a/platform/core/src/capabilities/InstantiationCapability.js b/platform/core/src/capabilities/InstantiationCapability.js index 52384e993e..82187a49bd 100644 --- a/platform/core/src/capabilities/InstantiationCapability.js +++ b/platform/core/src/capabilities/InstantiationCapability.js @@ -22,8 +22,8 @@ /*global define,Promise*/ define( - ['../objects/DomainObjectImpl', 'uuid'], - function (DomainObjectImpl, uuid) { + ['../objects/DomainObjectImpl'], + function (DomainObjectImpl) { 'use strict'; /** @@ -33,9 +33,12 @@ define( * @constructor * @memberof platform/core * @param $injector Angular's `$injector` + * @implements {Capability} */ - function InstantiationCapability($injector) { + function InstantiationCapability($injector, identifierService, domainObject) { this.$injector = $injector; + this.identifierService = identifierService; + this.domainObject = domainObject; } /** @@ -45,19 +48,26 @@ define( * have been persisted, nor will it have been added to the * composition of the object which exposed this capability. * + * @param {object} the model for the new domain object * @returns {DomainObject} the new domain object */ InstantiationCapability.prototype.instantiate = function (model) { + var parsedId = + this.identifierService.parse(this.domainObject.getId()), + space = parsedId.getDefinedSpace(), + id = this.identifierService.generate(space); + // Lazily initialize; instantiate depends on capabilityService, // which depends on all capabilities, including this one. this.instantiateFn = this.instantiateFn || this.$injector.get("instantiate"); - return this.instantiateFn(model); + + return this.instantiateFn(model, id); }; /** - * Alias of `create`. - * @see {platform/core.CreationCapability#create} + * Alias of `instantiate`. + * @see {platform/core.CreationCapability#instantiate} */ InstantiationCapability.prototype.invoke = InstantiationCapability.prototype.instantiate; diff --git a/platform/core/src/capabilities/PersistenceCapability.js b/platform/core/src/capabilities/PersistenceCapability.js index 74a6bc52e7..8bd29c7b7c 100644 --- a/platform/core/src/capabilities/PersistenceCapability.js +++ b/platform/core/src/capabilities/PersistenceCapability.js @@ -44,12 +44,16 @@ define( * @constructor * @implements {Capability} */ - function PersistenceCapability(persistenceService, space, domainObject) { + function PersistenceCapability( + persistenceService, + identifierService, + domainObject + ) { // Cache modified timestamp this.modified = domainObject.getModel().modified; this.domainObject = domainObject; - this.space = space; + this.identifierService = identifierService; this.persistenceService = persistenceService; } @@ -63,6 +67,11 @@ define( }; } + function getKey(id) { + var parts = id.split(":"); + return parts.length > 1 ? parts.slice(1).join(":") : id; + } + /** * Persist any changes which have been made to this * domain object's model. @@ -87,7 +96,7 @@ define( // ...and persist return persistenceFn.apply(persistenceService, [ this.getSpace(), - domainObject.getId(), + getKey(domainObject.getId()), domainObject.getModel() ]); }; @@ -130,7 +139,8 @@ define( * be used to persist this object */ PersistenceCapability.prototype.getSpace = function () { - return this.space; + var id = this.domainObject.getId(); + return this.identifierService.parse(id).getSpace(); }; return PersistenceCapability; diff --git a/platform/core/src/identifiers/Identifier.js b/platform/core/src/identifiers/Identifier.js new file mode 100644 index 0000000000..e587ee006b --- /dev/null +++ b/platform/core/src/identifiers/Identifier.js @@ -0,0 +1,86 @@ +/***************************************************************************** + * 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'; + + var SEPARATOR = ":"; + + /** + * Provides an interface for interpreting domain object identifiers; + * in particular, parses out persistence space/key pairs associated + * with the domain object. + * + * @memberof platform/core + * @constructor + * @param {string} id the domain object identifier + * @param {string} defaultSpace the persistence space to use if + * one is not encoded in the identifier + */ + function Identifier(id, defaultSpace) { + var separatorIndex = id.indexOf(SEPARATOR); + + if (separatorIndex > -1) { + this.key = id.substring(separatorIndex + 1); + this.space = id.substring(0, separatorIndex); + this.definedSpace = this.space; + } else { + this.key = id; + this.space = defaultSpace; + this.definedSpace = undefined; + } + } + + /** + * Get the key under which the identified domain object's model + * should be persisted, within its persistence space. + * @returns {string} the key within its persistence space + */ + Identifier.prototype.getKey = function () { + return this.key; + }; + + /** + * Get the space in which the identified domain object's model should + * be persisted. + * @returns {string} the persistence space + */ + Identifier.prototype.getSpace = function () { + return this.space; + }; + + /** + * Get the persistence space, if any, which has been explicitly + * encoded in this domain object's identifier. Returns undefined + * if no such space has been specified. + * @returns {string} the persistence space, or undefined + */ + Identifier.prototype.getDefinedSpace = function () { + return this.definedSpace; + }; + + return Identifier; + } +); diff --git a/platform/core/src/identifiers/IdentifierProvider.js b/platform/core/src/identifiers/IdentifierProvider.js new file mode 100644 index 0000000000..c6b2a136cb --- /dev/null +++ b/platform/core/src/identifiers/IdentifierProvider.js @@ -0,0 +1,66 @@ +/***************************************************************************** + * 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( + ["uuid", "./Identifier"], + function (uuid, Identifier) { + 'use strict'; + + /** + * Parses and generates domain object identifiers. + * @param {string} defaultSpace the default persistence space + * @constructor + * @memberof {platform/core} + */ + function IdentifierProvider(defaultSpace) { + this.defaultSpace = defaultSpace; + } + + /** + * Generate a new domain object identifier. A persistence space + * may optionally be included; if not specified, no space will + * be encoded into the identifier. + * @param {string} [space] the persistence space to encode + * in this identifier + * @returns {string} a new domain object identifier + */ + IdentifierProvider.prototype.generate = function (space) { + var id = uuid(); + if (space !== undefined) { + id = space + ":" + id; + } + return id; + }; + + /** + * Parse a domain object identifier to examine its component + * parts (e.g. its persistence space.) + * @returns {platform/core.Identifier} the parsed identifier + */ + IdentifierProvider.prototype.parse = function (id) { + return new Identifier(id, this.defaultSpace); + }; + + return IdentifierProvider; + } +); diff --git a/platform/core/src/models/PersistedModelProvider.js b/platform/core/src/models/PersistedModelProvider.js index c5e2927a96..3abe57841e 100644 --- a/platform/core/src/models/PersistedModelProvider.js +++ b/platform/core/src/models/PersistedModelProvider.js @@ -33,6 +33,15 @@ define( * A model service which reads domain object models from an external * persistence service. * + * Identifiers will be interpreted as follows: + * * If no colon is present, the model will be read from the default + * persistence space. + * * If a colon is present, everything before the first colon will be + * taken to refer to the persistence space, and everything after + * will be taken to be that model's key within this space. (If + * no such space exists within the `persistenceService`, that + * identifier will simply be ignored.) + * * @memberof platform/core * @constructor * @implements {ModelService} @@ -41,39 +50,26 @@ define( * @param $q Angular's $q service, for working with promises * @param {function} now a function which provides the current time * @param {string} space the name of the persistence space(s) - * from which models should be retrieved. - * @param {string} spaces additional persistence spaces to use + * from which models should be retrieved by default */ - function PersistedModelProvider(persistenceService, $q, now, space, spaces) { + function PersistedModelProvider(persistenceService, $q, now, space) { this.persistenceService = persistenceService; this.$q = $q; - this.spaces = [space].concat(spaces || []); this.now = now; - } - - // Take the most recently modified model, for cases where - // multiple persistence spaces return models. - function takeMostRecent(modelA, modelB) { - return (!modelB || modelB.modified === undefined) ? modelA : - (!modelA || modelA.modified === undefined) ? modelB : - modelB.modified > modelA.modified ? modelB : - modelA; + this.defaultSpace = space; } PersistedModelProvider.prototype.getModels = function (ids) { var persistenceService = this.persistenceService, $q = this.$q, - spaces = this.spaces, - space = this.space, - now = this.now; + now = this.now, + defaultSpace = this.defaultSpace, + parsedIds; // Load a single object model from any persistence spaces - function loadModel(id) { - return $q.all(spaces.map(function (space) { - return persistenceService.readObject(space, id); - })).then(function (models) { - return models.reduce(takeMostRecent); - }); + function loadModel(parsedId) { + return persistenceService + .readObject(parsedId.space, parsedId.key); } // Ensure that models read from persistence have some @@ -88,24 +84,43 @@ define( } // Package the result as id->model - function packageResult(models) { + function packageResult(parsedIds, models) { var result = {}; - ids.forEach(function (id, index) { + parsedIds.forEach(function (parsedId, index) { + var id = parsedId.id; if (models[index]) { - result[id] = addPersistedTimestamp(models[index]); + result[id] = models[index]; } }); return result; } - // Filter out "namespaced" identifiers; these are - // not expected to be found in database. See WTD-659. - ids = ids.filter(function (id) { - return id.indexOf(":") === -1; + function loadModels(parsedIds) { + return $q.all(parsedIds.map(loadModel)) + .then(function (models) { + return packageResult( + parsedIds, + models.map(addPersistedTimestamp) + ); + }); + } + + function restrictToSpaces(spaces) { + return parsedIds.filter(function (parsedId) { + return spaces.indexOf(parsedId.space) !== -1; + }); + } + + parsedIds = ids.map(function (id) { + var parts = id.split(":"); + return (parts.length > 1) ? + { id: id, space: parts[0], key: parts.slice(1).join(":") } : + { id: id, space: defaultSpace, key: id }; }); - // Give a promise for all persistence lookups... - return $q.all(ids.map(loadModel)).then(packageResult); + return persistenceService.listSpaces() + .then(restrictToSpaces) + .then(loadModels); }; return PersistedModelProvider; diff --git a/platform/core/src/services/Instantiate.js b/platform/core/src/services/Instantiate.js index f59916c938..c184e08f84 100644 --- a/platform/core/src/services/Instantiate.js +++ b/platform/core/src/services/Instantiate.js @@ -22,8 +22,8 @@ /*global define,Promise*/ define( - ['../objects/DomainObjectImpl', 'uuid'], - function (DomainObjectImpl, uuid) { + ['../objects/DomainObjectImpl'], + function (DomainObjectImpl) { 'use strict'; /** @@ -39,12 +39,15 @@ define( * * @constructor * @memberof platform/core - * @param $injector Angular's `$injector` + * @param {CapabilityService} capabilityService the service which will + * provide instantiated domain objects with their capabilities + * @param {IdentifierService} identifierService service to generate + * new identifiers */ - function Instantiate(capabilityService) { + function Instantiate(capabilityService, identifierService) { return function (model, id) { var capabilities = capabilityService.getCapabilities(model); - id = id || uuid(); + id = id || identifierService.generate(); return new DomainObjectImpl(id, model, capabilities); }; } diff --git a/platform/core/test/capabilities/InstantiationCapabilitySpec.js b/platform/core/test/capabilities/InstantiationCapabilitySpec.js index 0798a68f0c..35e0530b38 100644 --- a/platform/core/test/capabilities/InstantiationCapabilitySpec.js +++ b/platform/core/test/capabilities/InstantiationCapabilitySpec.js @@ -28,19 +28,40 @@ define( describe("The 'instantiation' capability", function () { var mockInjector, + mockIdentifierService, mockInstantiate, + mockIdentifier, + mockDomainObject, instantiation; beforeEach(function () { mockInjector = jasmine.createSpyObj("$injector", ["get"]); mockInstantiate = jasmine.createSpy("instantiate"); + mockIdentifierService = jasmine.createSpyObj( + 'identifierService', + [ 'parse', 'generate' ] + ); + mockIdentifier = jasmine.createSpyObj( + 'identifier', + [ 'getSpace', 'getKey', 'getDefinedSpace' ] + ); + mockDomainObject = jasmine.createSpyObj( + 'domainObject', + [ 'getId', 'getCapability', 'getModel' ] + ); mockInjector.get.andCallFake(function (key) { return key === 'instantiate' ? mockInstantiate : undefined; }); + mockIdentifierService.parse.andReturn(mockIdentifier); + mockIdentifierService.generate.andReturn("some-id"); - instantiation = new InstantiationCapability(mockInjector); + instantiation = new InstantiationCapability( + mockInjector, + mockIdentifierService, + mockDomainObject + ); }); @@ -59,7 +80,8 @@ define( mockInstantiate.andReturn(mockDomainObject); expect(instantiation.instantiate(testModel)) .toBe(mockDomainObject); - expect(mockInstantiate).toHaveBeenCalledWith(testModel); + expect(mockInstantiate) + .toHaveBeenCalledWith(testModel, jasmine.any(String)); }); }); diff --git a/platform/core/test/capabilities/PersistenceCapabilitySpec.js b/platform/core/test/capabilities/PersistenceCapabilitySpec.js index 253b011838..5b40e34c64 100644 --- a/platform/core/test/capabilities/PersistenceCapabilitySpec.js +++ b/platform/core/test/capabilities/PersistenceCapabilitySpec.js @@ -31,7 +31,9 @@ define( describe("The persistence capability", function () { var mockPersistenceService, + mockIdentifierService, mockDomainObject, + mockIdentifier, id = "object id", model = { someKey: "some value"}, SPACE = "some space", @@ -50,6 +52,14 @@ define( "persistenceService", [ "updateObject", "readObject", "createObject", "deleteObject" ] ); + mockIdentifierService = jasmine.createSpyObj( + 'identifierService', + [ 'parse', 'generate' ] + ); + mockIdentifier = jasmine.createSpyObj( + 'identifier', + [ 'getSpace', 'getKey', 'getDefinedSpace' ] + ); mockDomainObject = { getId: function () { return id; }, getModel: function () { return model; }, @@ -61,9 +71,11 @@ define( model = mutator(model) || model; } }); + mockIdentifierService.parse.andReturn(mockIdentifier); + mockIdentifier.getSpace.andReturn(SPACE); persistence = new PersistenceCapability( mockPersistenceService, - SPACE, + mockIdentifierService, mockDomainObject ); }); diff --git a/platform/core/test/identifiers/IdentifierProviderSpec.js b/platform/core/test/identifiers/IdentifierProviderSpec.js new file mode 100644 index 0000000000..c424984fef --- /dev/null +++ b/platform/core/test/identifiers/IdentifierProviderSpec.js @@ -0,0 +1,58 @@ +/***************************************************************************** + * 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/identifiers/IdentifierProvider"], + function (IdentifierProvider) { + 'use strict'; + + describe("IdentifierProvider", function () { + var defaultSpace, + provider; + + beforeEach(function () { + defaultSpace = "some-default-space"; + provider = new IdentifierProvider(defaultSpace); + }); + + it("generates unique identifiers", function () { + expect(provider.generate()) + .not.toEqual(provider.generate()); + }); + + it("allows spaces to be specified for generated identifiers", function () { + var specificSpace = "some-specific-space", + id = provider.generate(specificSpace); + expect(id).toEqual(jasmine.any(String)); + expect(provider.parse(id).getDefinedSpace()) + .toEqual(specificSpace); + }); + + it("parses identifiers using the default space", function () { + expect(provider.parse("some-unprefixed-id").getSpace()) + .toEqual(defaultSpace); + }); + + }); + } +); diff --git a/platform/core/test/identifiers/IdentifierSpec.js b/platform/core/test/identifiers/IdentifierSpec.js new file mode 100644 index 0000000000..81433f1072 --- /dev/null +++ b/platform/core/test/identifiers/IdentifierSpec.js @@ -0,0 +1,82 @@ +/***************************************************************************** + * 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/identifiers/Identifier"], + function (Identifier) { + 'use strict'; + + describe("A parsed domain object identifier", function () { + var id, + defaultSpace, + identifier; + + beforeEach(function () { + defaultSpace = "someDefaultSpace"; + }); + + describe("when space is encoded", function () { + var idSpace, idKey, spacedId; + + beforeEach(function () { + idSpace = "a-specific-space"; + idKey = "a-specific-key"; + id = idSpace + ":" + idKey; + identifier = new Identifier(id, defaultSpace); + }); + + it("provides the encoded space", function () { + expect(identifier.getSpace()).toEqual(idSpace); + }); + + it("provides the key within that space", function () { + expect(identifier.getKey()).toEqual(idKey); + }); + + it("provides the defined space", function () { + expect(identifier.getDefinedSpace()).toEqual(idSpace); + }); + }); + + describe("when space is not encoded", function () { + beforeEach(function () { + id = "a-generic-id"; + identifier = new Identifier(id, defaultSpace); + }); + + it("provides the default space", function () { + expect(identifier.getSpace()).toEqual(defaultSpace); + }); + + it("provides the id as the key", function () { + expect(identifier.getKey()).toEqual(id); + }); + + it("provides no defined space", function () { + expect(identifier.getDefinedSpace()).toEqual(undefined); + }); + }); + + }); + } +); diff --git a/platform/core/test/models/PersistedModelProviderSpec.js b/platform/core/test/models/PersistedModelProviderSpec.js index 81769834bf..c611a93bb2 100644 --- a/platform/core/test/models/PersistedModelProviderSpec.js +++ b/platform/core/test/models/PersistedModelProviderSpec.js @@ -33,13 +33,12 @@ define( var mockQ, mockPersistenceService, SPACE = "space0", - spaces = [ "space1" ], modTimes, mockNow, provider; function mockPromise(value) { - return { + return (value || {}).then ? value : { then: function (callback) { return mockPromise(callback(value)); }, @@ -78,13 +77,14 @@ define( persisted: 0 }); }); + mockPersistenceService.listSpaces + .andReturn(mockPromise([SPACE])); provider = new PersistedModelProvider( mockPersistenceService, mockQ, mockNow, - SPACE, - spaces + SPACE ); }); @@ -103,25 +103,6 @@ define( }); - it("reads object models from multiple spaces", function () { - var models; - - modTimes.space1 = { - 'x': 12321 - }; - - provider.getModels(["a", "x", "zz"]).then(function (m) { - models = m; - }); - - expect(models).toEqual({ - a: { space: SPACE, id: "a", persisted: 0 }, - x: { space: 'space1', id: "x", modified: 12321, persisted: 0 }, - zz: { space: SPACE, id: "zz", persisted: 0 } - }); - }); - - it("ensures that persisted timestamps are present", function () { var mockCallback = jasmine.createSpy("callback"), testModels = { diff --git a/platform/core/test/services/InstantiateSpec.js b/platform/core/test/services/InstantiateSpec.js index 31a5731dd3..cb25feaac2 100644 --- a/platform/core/test/services/InstantiateSpec.js +++ b/platform/core/test/services/InstantiateSpec.js @@ -29,18 +29,27 @@ define( describe("The 'instantiate' service", function () { var mockCapabilityService, + mockIdentifierService, mockCapabilityConstructor, mockCapabilityInstance, mockCapabilities, + mockIdentifier, + idCounter, testModel, instantiate, domainObject; beforeEach(function () { + idCounter = 0; + mockCapabilityService = jasmine.createSpyObj( 'capabilityService', ['getCapabilities'] ); + mockIdentifierService = jasmine.createSpyObj( + 'identifierService', + [ 'parse', 'generate' ] + ); mockCapabilityConstructor = jasmine.createSpy('capability'); mockCapabilityInstance = {}; mockCapabilityService.getCapabilities.andReturn({ @@ -48,9 +57,17 @@ define( }); mockCapabilityConstructor.andReturn(mockCapabilityInstance); + mockIdentifierService.generate.andCallFake(function (space) { + return (space ? (space + ":") : "") + + "some-id-" + (idCounter += 1); + }); + testModel = { someKey: "some value" }; - instantiate = new Instantiate(mockCapabilityService); + instantiate = new Instantiate( + mockCapabilityService, + mockIdentifierService + ); domainObject = instantiate(testModel); }); diff --git a/platform/core/test/suite.json b/platform/core/test/suite.json index d6afc373ef..b428fa5945 100644 --- a/platform/core/test/suite.json +++ b/platform/core/test/suite.json @@ -15,6 +15,9 @@ "capabilities/PersistenceCapability", "capabilities/RelationshipCapability", + "identifiers/Identifier", + "identifiers/IdentifierProvider", + "models/ModelAggregator", "models/MissingModelDecorator", "models/PersistedModelProvider", diff --git a/platform/entanglement/bundle.json b/platform/entanglement/bundle.json index 714d9f7f79..fc17ef9ef2 100644 --- a/platform/entanglement/bundle.json +++ b/platform/entanglement/bundle.json @@ -11,7 +11,7 @@ "glyph": "f", "category": "contextual", "implementation": "actions/MoveAction.js", - "depends": ["locationService", "moveService"] + "depends": ["policyService", "locationService", "moveService"] }, { "key": "copy", @@ -20,7 +20,7 @@ "glyph": "+", "category": "contextual", "implementation": "actions/CopyAction.js", - "depends": ["$log", "locationService", "copyService", + "depends": ["$log", "policyService", "locationService", "copyService", "dialogService", "notificationService"] }, { @@ -30,7 +30,7 @@ "glyph": "\u00E8", "category": "contextual", "implementation": "actions/LinkAction.js", - "depends": ["locationService", "linkService"] + "depends": ["policyService", "locationService", "linkService"] }, { "key": "follow", @@ -54,7 +54,11 @@ "depends": ["contextualize", "$q", "$log"] } ], - "controllers": [ + "policies": [ + { + "category": "action", + "implementation": "policies/CrossSpacePolicy.js" + } ], "capabilities": [ { diff --git a/platform/entanglement/src/actions/AbstractComposeAction.js b/platform/entanglement/src/actions/AbstractComposeAction.js index ef56c952b3..1dfe5c2567 100644 --- a/platform/entanglement/src/actions/AbstractComposeAction.js +++ b/platform/entanglement/src/actions/AbstractComposeAction.js @@ -62,6 +62,8 @@ define( * @constructor * @private * @memberof platform/entanglement + * @param {PolicyService} policyService the policy service to use to + * verify that variants of this action are allowed * @param {platform/entanglement.LocationService} locationService a * service to request destinations from the user * @param {platform/entanglement.AbstractComposeService} composeService @@ -71,7 +73,14 @@ define( * @param {string} [suffix] a string to display in the dialog title; * default is "to a new location" */ - function AbstractComposeAction(locationService, composeService, context, verb, suffix) { + function AbstractComposeAction( + policyService, + locationService, + composeService, + context, + verb, + suffix + ) { if (context.selectedObject) { this.newParent = context.domainObject; this.object = context.selectedObject; @@ -83,16 +92,27 @@ define( .getCapability('context') .getParent(); + this.context = context; + this.policyService = policyService; this.locationService = locationService; this.composeService = composeService; this.verb = verb || "Compose"; this.suffix = suffix || "to a new location"; } + AbstractComposeAction.prototype.cloneContext = function () { + var clone = {}, original = this.context; + Object.keys(original).forEach(function (k) { + clone[k] = original[k]; + }); + return clone; + }; + AbstractComposeAction.prototype.perform = function () { var dialogTitle, label, validateLocation, + self = this, locationService = this.locationService, composeService = this.composeService, currentParent = this.currentParent, @@ -109,7 +129,11 @@ define( label = this.verb + " To"; validateLocation = function (newParent) { - return composeService.validate(object, newParent); + var newContext = self.cloneContext(); + newContext.selectedObject = object; + newContext.domainObject = newParent; + return composeService.validate(object, newParent) && + self.policyService.allow("action", self, newContext); }; return locationService.getLocationFromUser( diff --git a/platform/entanglement/src/actions/CopyAction.js b/platform/entanglement/src/actions/CopyAction.js index 161db1ce27..a03b9cae23 100644 --- a/platform/entanglement/src/actions/CopyAction.js +++ b/platform/entanglement/src/actions/CopyAction.js @@ -34,18 +34,34 @@ define( * @constructor * @memberof platform/entanglement */ - function CopyAction($log, locationService, copyService, dialogService, - notificationService, context) { + function CopyAction( + $log, + policyService, + locationService, + copyService, + dialogService, + notificationService, + context + ) { this.dialog = undefined; this.notification = undefined; this.dialogService = dialogService; this.notificationService = notificationService; this.$log = $log; //Extend the behaviour of the Abstract Compose Action - AbstractComposeAction.call(this, locationService, copyService, - context, "Duplicate", "to a location"); + AbstractComposeAction.call( + this, + policyService, + locationService, + copyService, + context, + "Duplicate", + "to a location" + ); } + CopyAction.prototype = Object.create(AbstractComposeAction.prototype); + /** * Updates user about progress of copy. Should not be invoked by * client code under any circumstances. diff --git a/platform/entanglement/src/actions/LinkAction.js b/platform/entanglement/src/actions/LinkAction.js index f34e91f156..212f1e08fe 100644 --- a/platform/entanglement/src/actions/LinkAction.js +++ b/platform/entanglement/src/actions/LinkAction.js @@ -34,10 +34,10 @@ define( * @constructor * @memberof platform/entanglement */ - function LinkAction(locationService, linkService, context) { + function LinkAction(policyService, locationService, linkService, context) { AbstractComposeAction.apply( this, - [locationService, linkService, context, "Link"] + [policyService, locationService, linkService, context, "Link"] ); } diff --git a/platform/entanglement/src/actions/MoveAction.js b/platform/entanglement/src/actions/MoveAction.js index 1d090ce313..070dffbe78 100644 --- a/platform/entanglement/src/actions/MoveAction.js +++ b/platform/entanglement/src/actions/MoveAction.js @@ -34,12 +34,11 @@ define( * @constructor * @memberof platform/entanglement */ - function MoveAction(locationService, moveService, context) { + function MoveAction(policyService, locationService, moveService, context) { AbstractComposeAction.apply( this, - [locationService, moveService, context, "Move"] + [policyService, locationService, moveService, context, "Move"] ); - } MoveAction.prototype = Object.create(AbstractComposeAction.prototype); diff --git a/platform/entanglement/src/policies/CrossSpacePolicy.js b/platform/entanglement/src/policies/CrossSpacePolicy.js new file mode 100644 index 0000000000..a113972815 --- /dev/null +++ b/platform/entanglement/src/policies/CrossSpacePolicy.js @@ -0,0 +1,75 @@ +/***************************************************************************** + * 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'; + + var DISALLOWED_ACTIONS = [ + "move", + "copy", + "link", + "compose" + ]; + + /** + * This policy prevents performing move/copy/link actions across + * different persistence spaces (e.g. linking to an object in + * a private space from an object in a public space.) + * @memberof {platform/entanglement} + * @constructor + * @implements {Policy} + */ + function CrossSpacePolicy() { + } + + function lookupSpace(domainObject) { + var persistence = domainObject && + domainObject.getCapability("persistence"); + return persistence && persistence.getSpace(); + } + + function isCrossSpace(context) { + var domainObject = context.domainObject, + selectedObject = context.selectedObject, + spaces = [ domainObject, selectedObject ].map(lookupSpace); + return selectedObject !== undefined && + domainObject !== undefined && + lookupSpace(domainObject) !== lookupSpace(selectedObject); + } + + CrossSpacePolicy.prototype.allow = function (action, context) { + var key = action.getMetadata().key; + + if (DISALLOWED_ACTIONS.indexOf(key) !== -1) { + return !isCrossSpace(context); + } + + return true; + }; + + return CrossSpacePolicy; + + } +); diff --git a/platform/entanglement/test/actions/AbstractComposeActionSpec.js b/platform/entanglement/test/actions/AbstractComposeActionSpec.js index 6a0ddd7ebd..1e9bb014f3 100644 --- a/platform/entanglement/test/actions/AbstractComposeActionSpec.js +++ b/platform/entanglement/test/actions/AbstractComposeActionSpec.js @@ -34,6 +34,7 @@ define( describe("Move/copy/link Actions", function () { var action, + policyService, locationService, locationServicePromise, composeService, @@ -44,6 +45,11 @@ define( newParent; beforeEach(function () { + policyService = jasmine.createSpyObj( + 'policyService', + [ 'allow' ] + ); + selectedObjectContextCapability = jasmine.createSpyObj( 'selectedObjectContextCapability', [ @@ -87,6 +93,8 @@ define( ] ); + policyService.allow.andReturn(true); + locationService .getLocationFromUser .andReturn(locationServicePromise); @@ -124,6 +132,7 @@ define( }; action = new AbstractComposeAction( + policyService, locationService, composeService, context, @@ -164,6 +173,30 @@ define( expect(composeService.perform) .toHaveBeenCalledWith(selectedObject, newParent); }); + + describe("provides a validator which", function () { + var validator; + + beforeEach(function () { + validator = locationService.getLocationFromUser + .mostRecentCall.args[2]; + composeService.validate.andReturn(true); + policyService.allow.andReturn(true); + }); + + it("is sensitive to policy", function () { + expect(validator()).toBe(true); + policyService.allow.andReturn(false); + expect(validator()).toBe(false); + }); + + it("is sensitive to service-specific validation", function () { + expect(validator()).toBe(true); + composeService.validate.andReturn(false); + expect(validator()).toBe(false); + }); + + }); }); }); @@ -175,6 +208,7 @@ define( }; action = new AbstractComposeAction( + policyService, locationService, composeService, context, diff --git a/platform/entanglement/test/actions/CopyActionSpec.js b/platform/entanglement/test/actions/CopyActionSpec.js index fc8e615960..2ec1fde5a7 100644 --- a/platform/entanglement/test/actions/CopyActionSpec.js +++ b/platform/entanglement/test/actions/CopyActionSpec.js @@ -34,6 +34,7 @@ define( describe("Copy Action", function () { var copyAction, + policyService, locationService, locationServicePromise, copyService, @@ -50,6 +51,12 @@ define( progress = {phase: "copying", totalObjects: 10, processed: 1}; beforeEach(function () { + policyService = jasmine.createSpyObj( + 'policyService', + [ 'allow' ] + ); + policyService.allow.andReturn(true); + selectedObjectContextCapability = jasmine.createSpyObj( 'selectedObjectContextCapability', [ @@ -142,6 +149,7 @@ define( copyAction = new CopyAction( mockLog, + policyService, locationService, copyService, dialogService, @@ -201,6 +209,7 @@ define( copyAction = new CopyAction( mockLog, + policyService, locationService, copyService, dialogService, diff --git a/platform/entanglement/test/actions/LinkActionSpec.js b/platform/entanglement/test/actions/LinkActionSpec.js index 03967a6672..bf3bd05c6b 100644 --- a/platform/entanglement/test/actions/LinkActionSpec.js +++ b/platform/entanglement/test/actions/LinkActionSpec.js @@ -34,6 +34,7 @@ define( describe("Link Action", function () { var linkAction, + policyService, locationService, locationServicePromise, linkService, @@ -44,6 +45,12 @@ define( newParent; beforeEach(function () { + policyService = jasmine.createSpyObj( + 'policyService', + [ 'allow' ] + ); + policyService.allow.andReturn(true); + selectedObjectContextCapability = jasmine.createSpyObj( 'selectedObjectContextCapability', [ @@ -102,6 +109,7 @@ define( }; linkAction = new LinkAction( + policyService, locationService, linkService, context @@ -152,6 +160,7 @@ define( }; linkAction = new LinkAction( + policyService, locationService, linkService, context diff --git a/platform/entanglement/test/actions/MoveActionSpec.js b/platform/entanglement/test/actions/MoveActionSpec.js index 52a7c6e301..868a3ce84a 100644 --- a/platform/entanglement/test/actions/MoveActionSpec.js +++ b/platform/entanglement/test/actions/MoveActionSpec.js @@ -34,6 +34,7 @@ define( describe("Move Action", function () { var moveAction, + policyService, locationService, locationServicePromise, moveService, @@ -44,6 +45,12 @@ define( newParent; beforeEach(function () { + policyService = jasmine.createSpyObj( + 'policyService', + [ 'allow' ] + ); + policyService.allow.andReturn(true); + selectedObjectContextCapability = jasmine.createSpyObj( 'selectedObjectContextCapability', [ @@ -102,6 +109,7 @@ define( }; moveAction = new MoveAction( + policyService, locationService, moveService, context @@ -152,6 +160,7 @@ define( }; moveAction = new MoveAction( + policyService, locationService, moveService, context diff --git a/platform/entanglement/test/policies/CrossSpacePolicySpec.js b/platform/entanglement/test/policies/CrossSpacePolicySpec.js new file mode 100644 index 0000000000..214efc1cc3 --- /dev/null +++ b/platform/entanglement/test/policies/CrossSpacePolicySpec.js @@ -0,0 +1,120 @@ +/***************************************************************************** + * 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,spyOn */ +define( + [ + '../../src/policies/CrossSpacePolicy', + '../DomainObjectFactory' + ], + function (CrossSpacePolicy, domainObjectFactory) { + "use strict"; + + describe("CrossSpacePolicy", function () { + var mockAction, + testActionMetadata, + sameSpaceContext, + crossSpaceContext, + policy; + + function makeObject(space) { + var mockPersistence = jasmine.createSpyObj( + 'persistence', + ['getSpace'] + ); + mockPersistence.getSpace.andReturn(space); + return domainObjectFactory({ + id: space + ":foo", + model: {}, + capabilities: { persistence: mockPersistence } + }); + } + + beforeEach(function () { + testActionMetadata = {}; + + // Policy should only call passive methods, so + // only define those in mocks. + mockAction = jasmine.createSpyObj( + 'action', + [ 'getMetadata' ] + ); + mockAction.getMetadata.andReturn(testActionMetadata); + + sameSpaceContext = { + domainObject: makeObject('a'), + selectedObject: makeObject('a') + }; + crossSpaceContext = { + domainObject: makeObject('a'), + selectedObject: makeObject('b') + }; + + policy = new CrossSpacePolicy(); + }); + + ['move', 'copy', 'link', 'compose'].forEach(function (key) { + describe("for " + key + " actions", function () { + beforeEach(function () { + testActionMetadata.key = key; + }); + + it("allows same-space changes", function () { + expect(policy.allow(mockAction, sameSpaceContext)) + .toBe(true); + }); + + it("disallows cross-space changes", function () { + expect(policy.allow(mockAction, crossSpaceContext)) + .toBe(false); + }); + + it("allows actions with no selectedObject", function () { + expect(policy.allow(mockAction, { + domainObject: makeObject('a') + })).toBe(true); + }); + }); + }); + + describe("for other actions", function () { + beforeEach(function () { + testActionMetadata.key = "some-other-action"; + }); + + it("allows same-space and cross-space changes", function () { + expect(policy.allow(mockAction, crossSpaceContext)) + .toBe(true); + expect(policy.allow(mockAction, sameSpaceContext)) + .toBe(true); + }); + + it("allows actions with no selectedObject", function () { + expect(policy.allow(mockAction, { + domainObject: makeObject('a') + })).toBe(true); + }); + }); + + }); + } +); diff --git a/platform/entanglement/test/suite.json b/platform/entanglement/test/suite.json index 89c082f9c8..b954ab8ebc 100644 --- a/platform/entanglement/test/suite.json +++ b/platform/entanglement/test/suite.json @@ -4,6 +4,7 @@ "actions/GoToOriginalAction", "actions/LinkAction", "actions/MoveAction", + "policies/CrossSpacePolicy", "services/CopyService", "services/LinkService", "services/MoveService", diff --git a/platform/persistence/aggregator/bundle.json b/platform/persistence/aggregator/bundle.json new file mode 100644 index 0000000000..f9eaede5c1 --- /dev/null +++ b/platform/persistence/aggregator/bundle.json @@ -0,0 +1,12 @@ +{ + "extensions": { + "components": [ + { + "provides": "persistenceService", + "type": "aggregator", + "depends": [ "$q" ], + "implementation": "PersistenceAggregator.js" + } + ] + } +} diff --git a/platform/persistence/aggregator/src/PersistenceAggregator.js b/platform/persistence/aggregator/src/PersistenceAggregator.js new file mode 100644 index 0000000000..6ca463ecdf --- /dev/null +++ b/platform/persistence/aggregator/src/PersistenceAggregator.js @@ -0,0 +1,89 @@ +/***************************************************************************** + * 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,window*/ + +define( + [], + function () { + 'use strict'; + + // Return values to use when a persistence space is unknown, + // and there is no appropriate provider to route to. + var METHOD_DEFAULTS = { + createObject: false, + readObject: undefined, + listObjects: [], + updateObject: false, + deleteObject: false + }; + + /** + * Aggregates multiple persistence providers, such that they can be + * utilized as if they were a single object. This is achieved by + * routing persistence calls to an appropriate provider; the space + * specified at call time is matched with the first provider (per + * priority order) which reports that it provides persistence for + * this space. + * + * @memberof platform/persistence/aggregator + * @constructor + * @implements {PersistenceService} + * @param $q Angular's $q, for promises + * @param {PersistenceService[]} providers the providers to aggregate + */ + function PersistenceAggregator($q, providers) { + var providerMap = {}; + + function addToMap(provider) { + return provider.listSpaces().then(function (spaces) { + spaces.forEach(function (space) { + providerMap[space] = providerMap[space] || provider; + }); + }); + } + + this.providerMapPromise = $q.all(providers.map(addToMap)) + .then(function () { return providerMap; }); + } + + PersistenceAggregator.prototype.listSpaces = function () { + return this.providerMapPromise.then(function (map) { + return Object.keys(map); + }); + }; + + Object.keys(METHOD_DEFAULTS).forEach(function (method) { + PersistenceAggregator.prototype[method] = function (space) { + var delegateArgs = Array.prototype.slice.apply(arguments, []); + return this.providerMapPromise.then(function (map) { + var provider = map[space]; + return provider ? + provider[method].apply(provider, delegateArgs) : + METHOD_DEFAULTS[method]; + }); + }; + }); + + return PersistenceAggregator; + } +); diff --git a/platform/persistence/aggregator/test/PersistenceAggregatorSpec.js b/platform/persistence/aggregator/test/PersistenceAggregatorSpec.js new file mode 100644 index 0000000000..5e2b5c5fa8 --- /dev/null +++ b/platform/persistence/aggregator/test/PersistenceAggregatorSpec.js @@ -0,0 +1,103 @@ +/***************************************************************************** + * 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,spyOn */ +define( + ['../src/PersistenceAggregator'], + function (PersistenceAggregator) { + 'use strict'; + + var PERSISTENCE_SERVICE_METHODS = [ + 'listSpaces', + 'listObjects', + 'createObject', + 'readObject', + 'updateObject', + 'deleteObject' + ], + WRAPPED_METHODS = PERSISTENCE_SERVICE_METHODS.filter(function (m) { + return m !== 'listSpaces'; + }); + + function fakePromise(value) { + return (value || {}).then ? value : { + then: function (callback) { + return fakePromise(callback(value)); + } + }; + } + + describe("PersistenceAggregator", function () { + var mockQ, + mockProviders, + mockCallback, + testSpaces, + aggregator; + + beforeEach(function () { + testSpaces = ['a', 'b', 'c']; + mockQ = jasmine.createSpyObj("$q", ['all']); + mockProviders = testSpaces.map(function (space) { + var mockProvider = jasmine.createSpyObj( + 'provider-' + space, + PERSISTENCE_SERVICE_METHODS + ); + PERSISTENCE_SERVICE_METHODS.forEach(function (m) { + mockProvider[m].andReturn(fakePromise(true)); + }); + mockProvider.listSpaces.andReturn(fakePromise([space])); + return mockProvider; + }); + mockCallback = jasmine.createSpy(); + + mockQ.all.andCallFake(function (fakePromises) { + var result = []; + fakePromises.forEach(function (p) { + p.then(function (v) { result.push(v); }); + }); + return fakePromise(result); + }); + + aggregator = new PersistenceAggregator(mockQ, mockProviders); + }); + + it("exposes spaces for all providers", function () { + aggregator.listSpaces().then(mockCallback); + expect(mockCallback).toHaveBeenCalledWith(testSpaces); + }); + + WRAPPED_METHODS.forEach(function (m) { + it("redirects " + m + " calls to an appropriate provider", function () { + testSpaces.forEach(function (space, index) { + var key = 'key-' + space, + value = 'val-' + space; + expect(aggregator[m](space, key, value)) + .toEqual(mockProviders[index][m]()); + expect(mockProviders[index][m]) + .toHaveBeenCalledWith(space, key, value); + }); + }); + }); + + }); + } +); diff --git a/platform/persistence/aggregator/test/suite.json b/platform/persistence/aggregator/test/suite.json new file mode 100644 index 0000000000..7585e88ee9 --- /dev/null +++ b/platform/persistence/aggregator/test/suite.json @@ -0,0 +1,3 @@ +[ + "PersistenceAggregator" +]