Merge pull request #257 from nasa/open245b

[Persistence] Support multiple persistence spaces
This commit is contained in:
Victor Woeltjen 2015-11-21 07:13:54 -08:00
commit d6e2895666
35 changed files with 1113 additions and 97 deletions

View File

@ -24,6 +24,7 @@
"platform/features/events",
"platform/forms",
"platform/identity",
"platform/persistence/aggregator",
"platform/persistence/local",
"platform/persistence/queue",
"platform/policy",

View File

@ -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
<identifier> ::= <space> ":" <key> | <key>
<space> ::= <id char>+
<key> ::= <id char>+
<id char> ::= <letter> | <digit> | "-" | "." | "_"
```
## 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`

View File

@ -0,0 +1,2 @@
Example of using multiple persistence stores by exposing a root
object with a different space prefix.

View File

@ -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" ]
}
]
}
}

View File

@ -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;
}
);

View File

@ -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": [

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
);

View File

@ -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;
}
);

View File

@ -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;

View File

@ -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);
};
}

View File

@ -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));
});
});

View File

@ -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
);
});

View File

@ -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);
});
});
}
);

View File

@ -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);
});
});
});
}
);

View File

@ -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 = {

View File

@ -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);
});

View File

@ -15,6 +15,9 @@
"capabilities/PersistenceCapability",
"capabilities/RelationshipCapability",
"identifiers/Identifier",
"identifiers/IdentifierProvider",
"models/ModelAggregator",
"models/MissingModelDecorator",
"models/PersistedModelProvider",

View File

@ -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": [
{

View File

@ -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(

View File

@ -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.

View File

@ -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"]
);
}

View File

@ -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);

View File

@ -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;
}
);

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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);
});
});
});
}
);

View File

@ -4,6 +4,7 @@
"actions/GoToOriginalAction",
"actions/LinkAction",
"actions/MoveAction",
"policies/CrossSpacePolicy",
"services/CopyService",
"services/LinkService",
"services/MoveService",

View File

@ -0,0 +1,12 @@
{
"extensions": {
"components": [
{
"provides": "persistenceService",
"type": "aggregator",
"depends": [ "$q" ],
"implementation": "PersistenceAggregator.js"
}
]
}
}

View File

@ -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;
}
);

View File

@ -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);
});
});
});
});
}
);

View File

@ -0,0 +1,3 @@
[
"PersistenceAggregator"
]