diff --git a/index.html b/index.html index c51a4ba103..db553988e8 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,7 @@ openmct.install(openmct.plugins.Generator()); openmct.install(openmct.plugins.ExampleImagery()); openmct.install(openmct.plugins.UTCTimeSystem()); + openmct.install(openmct.plugins.ImportExport()); openmct.install(openmct.plugins.Conductor({ menuOptions: [ { diff --git a/platform/exporters/ExportService.js b/platform/exporters/ExportService.js index 4901444380..fc19424ee4 100644 --- a/platform/exporters/ExportService.js +++ b/platform/exporters/ExportService.js @@ -65,6 +65,20 @@ define(['csv'], function (CSV) { this.saveAs(blob, filename); }; + /** + * Export an object as a JSON file. Triggers a download using the function + * provided when the ExportService was instantiated. + * + * @param {Object} obj an object to be exported as JSON + * @param {ExportOptions} [options] additional parameters for the file + * export + */ + ExportService.prototype.exportJSON = function (obj, options) { + var filename = (options && options.filename) || "test-export.json"; + var jsonText = JSON.stringify(obj); + var blob = new Blob([jsonText], {type: "application/json"}); + this.saveAs(blob, filename); + }; /** * Additional parameters for file export. * @typedef ExportOptions diff --git a/platform/forms/bundle.js b/platform/forms/bundle.js index ab8ceca0da..0c6b7dcaed 100644 --- a/platform/forms/bundle.js +++ b/platform/forms/bundle.js @@ -24,6 +24,8 @@ define([ "./src/MCTForm", "./src/MCTToolbar", "./src/MCTControl", + "./src/MCTFileInput", + "./src/FileInputService", "./src/controllers/AutocompleteController", "./src/controllers/DateTimeController", "./src/controllers/CompositeController", @@ -42,11 +44,14 @@ define([ "text!./res/templates/controls/menu-button.html", "text!./res/templates/controls/dialog.html", "text!./res/templates/controls/radio.html", + "text!./res/templates/controls/file-input.html", 'legacyRegistry' ], function ( MCTForm, MCTToolbar, MCTControl, + MCTFileInput, + FileInputService, AutocompleteController, DateTimeController, CompositeController, @@ -65,6 +70,7 @@ define([ menuButtonTemplate, dialogTemplate, radioTemplate, + fileInputTemplate, legacyRegistry ) { @@ -88,6 +94,13 @@ define([ "templateLinker", "controls[]" ] + }, + { + "key": "mctFileInput", + "implementation": MCTFileInput, + "depends": [ + "fileInputService" + ] } ], "controls": [ @@ -142,6 +155,10 @@ define([ { "key": "dialog-button", "template": dialogTemplate + }, + { + "key": "file-input", + "template": fileInputTemplate } ], "controllers": [ @@ -176,6 +193,14 @@ define([ "dialogService" ] } + ], + "components": [ + { + "provides": "fileInputService", + "type": "provider", + "implementation": FileInputService + } + ] } }); diff --git a/platform/forms/res/templates/controls/file-input.html b/platform/forms/res/templates/controls/file-input.html new file mode 100644 index 0000000000..4d37d91f85 --- /dev/null +++ b/platform/forms/res/templates/controls/file-input.html @@ -0,0 +1,30 @@ + + + + + {{structure.text}} + + diff --git a/platform/forms/src/FileInputService.js b/platform/forms/src/FileInputService.js new file mode 100644 index 0000000000..22a94cbb0d --- /dev/null +++ b/platform/forms/src/FileInputService.js @@ -0,0 +1,90 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define(["zepto"], function ($) { + + /** + * The FileInputService provides an interface for triggering a file input. + * + * @constructor + * @memberof platform/forms + */ + function FileInputService() { + + } + + /** + * Creates, triggers, and destroys a file picker element and returns a + * promise for an object containing the chosen file's name and contents. + * + * @returns {Promise} promise for an object containing file meta-data + */ + FileInputService.prototype.getInput = function () { + var input = this.newInput(); + var read = this.readFile; + var fileInfo = {}; + var file; + + return new Promise(function (resolve, reject) { + input.trigger("click"); + input.on('change', function (event) { + file = this.files[0]; + input.remove(); + if (file) { + read(file) + .then(function (contents) { + fileInfo.name = file.name; + fileInfo.body = contents; + resolve(fileInfo); + }, function () { + reject("File read error"); + }); + } + }); + }); + }; + + FileInputService.prototype.readFile = function (file) { + var fileReader = new FileReader(); + + return new Promise(function (resolve, reject) { + fileReader.onload = function (event) { + resolve(event.target.result); + }; + + fileReader.onerror = function () { + return reject(event.target.result); + }; + fileReader.readAsText(file); + }); + }; + + FileInputService.prototype.newInput = function () { + var input = $(document.createElement('input')); + input.attr("type", "file"); + input.css("display", "none"); + $('body').append(input); + return input; + }; + + return FileInputService; +}); diff --git a/platform/forms/src/MCTFileInput.js b/platform/forms/src/MCTFileInput.js new file mode 100644 index 0000000000..92ef77c2be --- /dev/null +++ b/platform/forms/src/MCTFileInput.js @@ -0,0 +1,66 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + ['zepto'], + function ($) { + + /** + * The mct-file-input handles behavior of the file input form control. + * @constructor + * @memberof platform/forms + */ + function MCTFileInput(fileInputService) { + + function link(scope, element, attrs, control) { + + function setText(fileName) { + scope.structure.text = fileName.length > 20 ? + fileName.substr(0, 20) + "..." : + fileName; + } + + function handleClick() { + fileInputService.getInput().then(function (result) { + setText(result.name); + scope.ngModel[scope.field] = result; + control.$setValidity("file-input", true); + }, function () { + setText('Select File'); + control.$setValidity("file-input", false); + }); + } + + control.$setValidity("file-input", false); + element.on('click', handleClick); + } + + return { + restrict: "A", + require: "^form", + link: link + }; + } + + return MCTFileInput; + } +); diff --git a/platform/forms/test/FileInputServiceSpec.js b/platform/forms/test/FileInputServiceSpec.js new file mode 100644 index 0000000000..0a738bf5ef --- /dev/null +++ b/platform/forms/test/FileInputServiceSpec.js @@ -0,0 +1,74 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + ["../src/FileInputService"], + function (FileInputService) { + + describe("The FileInputService", function () { + var fileInputService, + mockInput; + + beforeEach(function () { + fileInputService = new FileInputService(); + mockInput = jasmine.createSpyObj('input', + [ + 'on', + 'trigger', + 'remove' + ] + ); + mockInput.on.andCallFake(function (event, changeHandler) { + changeHandler.apply(mockInput); + }); + spyOn(fileInputService, "newInput").andReturn( + mockInput + ); + + }); + + it("can read a file", function () { + mockInput.files = [new File(["file content"], "file name")]; + fileInputService.getInput().then(function (result) { + expect(result.name).toBe("file name"); + expect(result.body).toBe("file content"); + }); + + expect(mockInput.trigger).toHaveBeenCalledWith('click'); + expect(mockInput.remove).toHaveBeenCalled(); + }); + + it("catches file read errors", function () { + mockInput.files = ["GARBAGE"]; + fileInputService.getInput().then( + function (result) {}, + function (err) { + expect(err).toBe("File read error"); + } + ); + + expect(mockInput.trigger).toHaveBeenCalledWith('click'); + expect(mockInput.remove).toHaveBeenCalled(); + }); + }); + } +); diff --git a/platform/forms/test/MCTFileInputSpec.js b/platform/forms/test/MCTFileInputSpec.js new file mode 100644 index 0000000000..311c6007cd --- /dev/null +++ b/platform/forms/test/MCTFileInputSpec.js @@ -0,0 +1,98 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + ["../src/MCTFileInput"], + function (MCTFileInput) { + + describe("The mct-file-input directive", function () { + + var mockScope, + mockFileInputService, + mctFileInput, + element, + attrs, + control; + + beforeEach(function () { + attrs = []; + control = jasmine.createSpyObj('control', ['$setValidity']); + element = jasmine.createSpyObj('element', ['on', 'trigger']); + mockFileInputService = jasmine.createSpyObj('fileInputService', + ['getInput'] + ); + mockScope = jasmine.createSpyObj( + '$scope', + ['$watch'] + ); + + mockScope.structure = {text: 'Select File'}; + mockScope.field = "file-input"; + mockScope.ngModel = {"file-input" : undefined}; + + element.on.andCallFake(function (event, clickHandler) { + clickHandler(); + }); + mockFileInputService.getInput.andReturn( + Promise.resolve({name: "file-name", body: "file-body"}) + ); + + mctFileInput = new MCTFileInput(mockFileInputService); + + // Need to wait for mock promise + var init = false; + runs(function () { + mctFileInput.link(mockScope, element, attrs, control); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "File selection should have beeen simulated"); + }); + + it("is restricted to attributes", function () { + expect(mctFileInput.restrict).toEqual("A"); + }); + + it("changes button text to match file name", function () { + expect(element.on).toHaveBeenCalledWith( + 'click', + jasmine.any(Function) + ); + expect(mockScope.structure.text).toEqual("file-name"); + }); + + it("validates control on file selection", function () { + expect(control.$setValidity.callCount).toBe(2); + expect(control.$setValidity.argsForCall[0]).toEqual( + ['file-input', false] + ); + expect(control.$setValidity.argsForCall[1]).toEqual( + ['file-input', true] + ); + }); + }); + } +); diff --git a/platform/import-export/bundle.js b/platform/import-export/bundle.js new file mode 100644 index 0000000000..255d122a12 --- /dev/null +++ b/platform/import-export/bundle.js @@ -0,0 +1,76 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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([ + "./src/actions/ExportAsJSONAction", + "./src/actions/ImportAsJSONAction" +], function ( + ExportAsJSONAction, + ImportAsJSONAction +) { + + return function ImportExportPlugin() { + return function (openmct) { + ExportAsJSONAction.appliesTo = function (context) { + return openmct.$injector.get('policyService') + .allow("creation", context.domainObject.getCapability("type") + ); + }; + + openmct.legacyRegistry.register("platform/import-export", { + "name": "Import-export plugin", + "description": "Allows importing / exporting of domain objects as JSON.", + "extensions": { + "actions": [ + { + "key": "export.JSON", + "name": "Export as JSON", + "implementation": ExportAsJSONAction, + "category": "contextual", + "cssClass": "icon-save", + "depends": [ + "exportService", + "policyService", + "identifierService" + ] + }, + { + "key": "import.JSON", + "name": "Import from JSON", + "implementation": ImportAsJSONAction, + "category": "contextual", + "cssClass": "icon-download", + "depends": [ + "exportService", + "identifierService", + "dialogService", + "openmct" + ] + } + ] + } + }); + openmct.legacyRegistry.enable('platform/import-export'); + }; + }; +}); diff --git a/platform/import-export/src/actions/ExportAsJSONAction.js b/platform/import-export/src/actions/ExportAsJSONAction.js new file mode 100644 index 0000000000..56de7460fe --- /dev/null +++ b/platform/import-export/src/actions/ExportAsJSONAction.js @@ -0,0 +1,162 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define([], function () { + + /** + * The ExportAsJSONAction is available from context menus and allows a user + * to export any creatable domain object as a JSON file. + * + * @implements {Action} + * @constructor + * @memberof platform/import-export + */ + function ExportAsJSONAction( + exportService, + policyService, + identifierService, + context + ) { + + this.root = {}; + this.tree = {}; + this.calls = 0; + this.context = context; + this.externalIdentifiers = []; + this.exportService = exportService; + this.policyService = policyService; + this.identifierService = identifierService; + } + + ExportAsJSONAction.prototype.perform = function () { + this.root = this.context.domainObject; + this.tree[this.root.getId()] = this.root.getModel(); + this.saveAs = function (completedTree) { + this.exportService.exportJSON( + completedTree, + {filename: this.root.getModel().name + '.json'} + ); + }; + + this.write(this.root); + }; + + /** + * Traverses object hierarchy and populates tree object with models and + * identifiers. + * + * @private + * @param {Object} parent + */ + ExportAsJSONAction.prototype.write = function (parent) { + + this.calls++; + if (parent.hasCapability('composition')) { + parent.useCapability('composition') + .then(function (children) { + children.forEach(function (child, index) { + // Only export if object is creatable + if (this.isCreatable(child)) { + // Prevents infinite export of self-contained objs + if (!this.tree.hasOwnProperty(child.getId())) { + // If object is a link to something absent from + // tree, generate new id and treat as new object + if (this.isExternal(child, parent)) { + this.rewriteLink(child, parent); + } else { + this.tree[child.getId()] = child.getModel(); + } + this.write(child); + } + } + }.bind(this)); + this.calls--; + if (this.calls === 0) { + this.saveAs(this.wrapTree()); + } + }.bind(this)); + } else { + this.calls--; + if (this.calls === 0) { + this.saveAs(this.wrapTree()); + } + } + }; + + /** + * Exports an externally linked object as an entirely new object in the + * case where the original is not present in the exported tree. + * + * @private + */ + ExportAsJSONAction.prototype.rewriteLink = function (child, parent) { + this.externalIdentifiers.push(child.getId()); + var parentModel = parent.getModel(); + var childModel = child.getModel(); + var index = parentModel.composition.indexOf(child.getId()); + var newModel = this.copyModel(childModel); + var newId = this.identifierService.generate(); + + newModel.location = parent.getId(); + this.tree[newId] = newModel; + this.tree[parent.getId()] = this.copyModel(parentModel); + this.tree[parent.getId()].composition[index] = newId; + }; + + ExportAsJSONAction.prototype.copyModel = function (model) { + var jsonString = JSON.stringify(model); + return JSON.parse(jsonString); + }; + + ExportAsJSONAction.prototype.isExternal = function (child, parent) { + if (child.getModel().location !== parent.getId() && + !Object.keys(this.tree).includes(child.getModel().location) && + child.getId() !== this.root.getId() || + this.externalIdentifiers.includes(child.getId())) { + + return true; + } + return false; + }; + + /** + * Wraps root object for identification on reimport and wraps entire + * exported JSON construct for validation. + * + * @private + */ + ExportAsJSONAction.prototype.wrapTree = function () { + return { + "openmct": this.tree, + "rootId": this.root.getId() + }; + }; + + ExportAsJSONAction.prototype.isCreatable = function (domainObject) { + return this.policyService.allow( + "creation", + domainObject.getCapability("type") + ); + }; + + return ExportAsJSONAction; +}); diff --git a/platform/import-export/src/actions/ImportAsJSONAction.js b/platform/import-export/src/actions/ImportAsJSONAction.js new file mode 100644 index 0000000000..03467fef34 --- /dev/null +++ b/platform/import-export/src/actions/ImportAsJSONAction.js @@ -0,0 +1,175 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define(['zepto'], function ($) { + + /** + * The ImportAsJSONAction is available from context menus and allows a user + * to import a previously exported domain object into any domain object + * that has the composition capability. + * + * @implements {Action} + * @constructor + * @memberof platform/import-export + */ + function ImportAsJSONAction( + exportService, + identifierService, + dialogService, + openmct, + context + ) { + + this.openmct = openmct; + this.context = context; + this.exportService = exportService; + this.dialogService = dialogService; + this.identifierService = identifierService; + this.instantiate = openmct.$injector.get("instantiate"); + } + + ImportAsJSONAction.prototype.perform = function () { + this.dialogService.getUserInput(this.getFormModel(), {}) + .then(function (form) { + var objectTree = form.selectFile.body; + if (this.validateJSON(objectTree)) { + this.importObjectTree(JSON.parse(objectTree)); + } else { + this.displayError(); + } + }.bind(this)); + }; + + ImportAsJSONAction.prototype.importObjectTree = function (objTree) { + var parent = this.context.domainObject; + var tree = this.generateNewIdentifiers(objTree); + var rootId = tree.rootId; + var rootObj = this.instantiate(tree.openmct[rootId], rootId); + + // Instantiate all objects in tree with their newly genereated ids, + // adding each to its rightful parent's composition + rootObj.getCapability("location").setPrimaryLocation(parent.getId()); + this.deepInstantiate(rootObj, tree.openmct, []); + parent.getCapability("composition").add(rootObj); + }; + + ImportAsJSONAction.prototype.deepInstantiate = function (parent, tree, seen) { + // Traverses object tree, instantiates all domain object w/ new IDs and + // adds to parent's composition + if (parent.hasCapability("composition")) { + var parentModel = parent.getModel(); + var newObj; + + seen.push(parent.getId()); + parentModel.composition.forEach(function (childId, index) { + if (!tree[childId] || seen.includes(childId)) { + return; + } + + newObj = this.instantiate(tree[childId], childId); + parent.getCapability("composition").add(newObj); + newObj.getCapability("location") + .setPrimaryLocation(tree[childId].location); + this.deepInstantiate(newObj, tree, seen); + }, this); + } + }; + + ImportAsJSONAction.prototype.generateNewIdentifiers = function (tree) { + // For each domain object in the file, generate new ID, replace in tree + Object.keys(tree.openmct).forEach(function (domainObjectId) { + var newId = this.identifierService.generate(); + tree = this.rewriteId(domainObjectId, newId, tree); + }, this); + return tree; + }; + + /** + * Rewrites all instances of a given id in the tree with a newly generated + * replacement to prevent collision. + * + * @private + */ + ImportAsJSONAction.prototype.rewriteId = function (oldID, newID, tree) { + tree = JSON.stringify(tree).replace(new RegExp(oldID, 'g'), newID); + return JSON.parse(tree); + }; + + ImportAsJSONAction.prototype.getFormModel = function () { + return { + name: "Import as JSON", + sections: [ + { + name: "Import A File", + rows: [ + { + name: 'Select File', + key: 'selectFile', + control: 'file-input', + required: true, + text: 'Select File' + } + ] + } + ] + }; + }; + + ImportAsJSONAction.prototype.validateJSON = function (jsonString) { + var json; + try { + json = JSON.parse(jsonString); + } catch (e) { + return false; + } + if (!json.openmct || !json.rootId) { + return false; + } + return true; + }; + + ImportAsJSONAction.prototype.displayError = function () { + var dialog, + model = { + title: "Invalid File", + actionText: "The selected file was either invalid JSON or was " + + "not formatted properly for import into Open MCT.", + severity: "error", + options: [ + { + label: "Ok", + callback: function () { + dialog.dismiss(); + } + } + ] + }; + dialog = this.dialogService.showBlockingMessage(model); + }; + + ImportAsJSONAction.appliesTo = function (context) { + return context.domainObject !== undefined && + context.domainObject.hasCapability("composition"); + }; + + return ImportAsJSONAction; +}); diff --git a/platform/import-export/test/actions/ExportAsJSONActionSpec.js b/platform/import-export/test/actions/ExportAsJSONActionSpec.js new file mode 100644 index 0000000000..146f82b244 --- /dev/null +++ b/platform/import-export/test/actions/ExportAsJSONActionSpec.js @@ -0,0 +1,266 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + [ + "../../src/actions/ExportAsJSONAction", + "../../../entanglement/test/DomainObjectFactory" + ], + function (ExportAsJSONAction, domainObjectFactory) { + + describe("The export JSON action", function () { + + var context, + action, + exportService, + identifierService, + policyService, + mockType, + exportedTree; + + beforeEach(function () { + exportService = jasmine.createSpyObj('exportService', + ['exportJSON']); + identifierService = jasmine.createSpyObj('identifierService', + ['generate']); + policyService = jasmine.createSpyObj('policyService', + ['allow']); + mockType = + jasmine.createSpyObj('type', ['hasFeature']); + + mockType.hasFeature.andCallFake(function (feature) { + return feature === 'creation'; + }); + context = {}; + context.domainObject = domainObjectFactory( + { + name: 'test', + id: 'someID', + capabilities: {type: mockType} + }); + identifierService.generate.andReturn('brandNewId'); + exportService.exportJSON.andCallFake(function (tree, options) { + exportedTree = tree; + }); + policyService.allow.andCallFake(function (capability, type) { + return type.hasFeature(capability); + }); + + action = new ExportAsJSONAction(exportService, policyService, + identifierService, context); + }); + + it("initializes happily", function () { + expect(action).toBeDefined(); + }); + + it("doesn't export non-creatable objects in tree", function () { + var nonCreatableType = { + hasFeature : + function (feature) { + return feature !== 'creation'; + } + }; + + var parentComposition = + jasmine.createSpyObj('parentComposition', ['invoke']); + + var parent = domainObjectFactory({ + name: 'parent', + model: { name: 'parent', location: 'ROOT'}, + id: 'parentId', + capabilities: { + composition: parentComposition, + type: mockType + } + }); + + var child = domainObjectFactory({ + name: 'child', + model: { name: 'child', location: 'parentId' }, + id: 'childId', + capabilities: { + type: nonCreatableType + } + }); + + parentComposition.invoke.andReturn( + Promise.resolve([child]) + ); + context.domainObject = parent; + + var init = false; + runs(function () { + action.perform(); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "Exported tree sohuld have been built"); + + runs(function () { + expect(Object.keys(action.tree).length).toBe(1); + expect(action.tree.hasOwnProperty("parentId")) + .toBeTruthy(); + }); + }); + + it("can export self-containing objects", function () { + var infiniteParentComposition = + jasmine.createSpyObj('infiniteParentComposition', + ['invoke'] + ); + + var infiniteChildComposition = + jasmine.createSpyObj('infiniteChildComposition', + ['invoke'] + ); + + var parent = domainObjectFactory({ + name: 'parent', + model: { name: 'parent', location: 'ROOT'}, + id: 'infiniteParentId', + capabilities: { + composition: infiniteParentComposition, + type: mockType + } + }); + + var child = domainObjectFactory({ + name: 'child', + model: { name: 'child', location: 'infiniteParentId' }, + id: 'infiniteChildId', + capabilities: { + composition: infiniteChildComposition, + type: mockType + } + }); + + infiniteParentComposition.invoke.andReturn( + Promise.resolve([child]) + ); + infiniteChildComposition.invoke.andReturn( + Promise.resolve([parent]) + ); + context.domainObject = parent; + + var init = false; + runs(function () { + action.perform(); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "Exported tree sohuld have been built"); + + runs(function () { + expect(Object.keys(action.tree).length).toBe(2); + expect(action.tree.hasOwnProperty("infiniteParentId")) + .toBeTruthy(); + expect(action.tree.hasOwnProperty("infiniteChildId")) + .toBeTruthy(); + }); + }); + + it("exports links to external objects as new objects", function () { + var externallyLinkedComposition = + jasmine.createSpyObj('externallyLinkedComposition', + ['invoke'] + ); + + var parent = domainObjectFactory({ + name: 'parent', + model: { + name: 'parent', + composition: ['externalId'], + location: 'ROOT'}, + id: 'parentId', + capabilities: { + composition: externallyLinkedComposition, + type: mockType + } + }); + + var externalObject = domainObjectFactory({ + name: 'external', + model: { name: 'external', location: 'outsideOfTree'}, + id: 'externalId', + capabilities: { + type: mockType + } + }); + + externallyLinkedComposition.invoke.andReturn( + Promise.resolve([externalObject]) + ); + context.domainObject = parent; + + var init = false; + runs(function () { + action.perform(); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "Exported tree sohuld have been built"); + + runs(function () { + expect(Object.keys(action.tree).length).toBe(2); + expect(action.tree.hasOwnProperty('parentId')) + .toBeTruthy(); + expect(action.tree.hasOwnProperty('brandNewId')) + .toBeTruthy(); + expect(action.tree.brandNewId.location).toBe('parentId'); + }); + }); + + it("exports object tree in the correct format", function () { + var init = false; + runs(function () { + action.perform(); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "Exported tree sohuld have been built"); + + runs(function () { + expect(Object.keys(exportedTree).length).toBe(2); + expect(exportedTree.hasOwnProperty('openmct')).toBeTruthy(); + expect(exportedTree.hasOwnProperty('rootId')).toBeTruthy(); + }); + }); + }); + } +); diff --git a/platform/import-export/test/actions/ImportAsJSONActionSpec.js b/platform/import-export/test/actions/ImportAsJSONActionSpec.js new file mode 100644 index 0000000000..785a2a5706 --- /dev/null +++ b/platform/import-export/test/actions/ImportAsJSONActionSpec.js @@ -0,0 +1,240 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define( + [ + "../../src/actions/ImportAsJSONAction", + "../../../entanglement/test/DomainObjectFactory" + ], + function (ImportAsJSONAction, domainObjectFactory) { + + describe("The import JSON action", function () { + + var context = {}; + var action, + exportService, + identifierService, + dialogService, + openmct, + mockDialog, + compositionCapability, + mockInstantiate, + uniqueId, + newObjects; + + + beforeEach(function () { + + uniqueId = 0; + newObjects = []; + openmct = { + $injector: jasmine.createSpyObj('$injector', ['get']) + }; + mockInstantiate = jasmine.createSpy('instantiate').andCallFake( + function (model, id) { + var config = { + "model": model, + "id": id, + "capabilities": {} + }; + var locationCapability = { + setPrimaryLocation: jasmine.createSpy + ('setPrimaryLocation').andCallFake( + function (newLocation) { + config.model.location = newLocation; + } + ) + }; + config.capabilities.location = locationCapability; + if (model.composition) { + var compCapability = + jasmine.createSpy('compCapability') + .andReturn(model.composition); + compCapability.add = jasmine.createSpy('add') + .andCallFake(function (newObj) { + config.model.composition.push(newObj.getId()); + }); + config.capabilities.composition = compCapability; + } + newObjects.push(domainObjectFactory(config)); + return domainObjectFactory(config); + }); + openmct.$injector.get.andReturn(mockInstantiate); + dialogService = jasmine.createSpyObj('dialogService', + [ + 'getUserInput', + 'showBlockingMessage' + ] + ); + identifierService = jasmine.createSpyObj('identifierService', + [ + 'generate' + ] + ); + identifierService.generate.andCallFake(function () { + uniqueId++; + return uniqueId; + }); + compositionCapability = jasmine.createSpy('compositionCapability'); + mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]); + dialogService.showBlockingMessage.andReturn(mockDialog); + + action = new ImportAsJSONAction(exportService, identifierService, + dialogService, openmct, context); + }); + + it("initializes happily", function () { + expect(action).toBeDefined(); + }); + + it("only applies to objects with composition capability", function () { + var compDomainObject = domainObjectFactory({ + name: 'compObject', + model: { name: 'compObject'}, + capabilities: {"composition": compositionCapability} + }); + var noCompDomainObject = domainObjectFactory(); + + context.domainObject = compDomainObject; + expect(ImportAsJSONAction.appliesTo(context)).toBe(true); + context.domainObject = noCompDomainObject; + expect(ImportAsJSONAction.appliesTo(context)).toBe(false); + }); + + it("displays error dialog on invalid file choice", function () { + dialogService.getUserInput.andReturn(Promise.resolve( + { + selectFile: { + body: JSON.stringify({badKey: "INVALID"}), + name: "fileName" + } + }) + ); + + var init = false; + runs(function () { + action.perform(); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "Promise containing file data should have resolved"); + + runs(function () { + expect(dialogService.getUserInput).toHaveBeenCalled(); + expect(dialogService.showBlockingMessage).toHaveBeenCalled(); + }); + }); + + it("can import self-containing objects", function () { + dialogService.getUserInput.andReturn(Promise.resolve( + { + selectFile: { + body: JSON.stringify({ + "openmct": { + "infiniteParent": { + "composition": ["infinteChild"], + "name": "1", + "type": "folder", + "modified": 1503598129176, + "location": "mine", + "persisted": 1503598129176 + }, + "infinteChild": { + "composition": ["infiniteParent"], + "name": "2", + "type": "folder", + "modified": 1503598132428, + "location": "infiniteParent", + "persisted": 1503598132428 + } + }, + "rootId": "infiniteParent" + }), + name: "fileName" + } + }) + ); + + var init = false; + runs(function () { + action.perform(); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "Promise containing file data should have resolved"); + + runs(function () { + expect(mockInstantiate.calls.length).toEqual(2); + }); + }); + + it("assigns new ids to each imported object", function () { + dialogService.getUserInput.andReturn(Promise.resolve( + { + selectFile: { + body: JSON.stringify({ + "openmct": { + "cce9f107-5060-4f55-8151-a00120f4222f": { + "composition": [], + "name": "test", + "type": "folder", + "modified": 1503596596639, + "location": "mine", + "persisted": 1503596596639 + } + }, + "rootId": "cce9f107-5060-4f55-8151-a00120f4222f" + }), + name: "fileName" + } + }) + ); + + var init = false; + runs(function () { + action.perform(); + setTimeout(function () { + init = true; + }, 100); + }); + + waitsFor(function () { + return init; + }, "Promise containing file data should have resolved"); + + runs(function () { + expect(mockInstantiate.calls.length).toEqual(1); + expect(newObjects[0].getId()).toBe('1'); + }); + }); + + }); + } +); diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index ee3cbe42c7..b381a1cfc3 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -26,14 +26,16 @@ define([ '../../example/generator/plugin', '../../platform/features/autoflow/plugin', './timeConductor/plugin', - '../../example/imagery/plugin' + '../../example/imagery/plugin', + '../../platform/import-export/bundle' ], function ( _, UTCTimeSystem, GeneratorPlugin, AutoflowPlugin, TimeConductorPlugin, - ExampleImagery + ExampleImagery, + ImportExport ) { var bundleMap = { CouchDB: 'platform/persistence/couch', @@ -54,6 +56,8 @@ define([ plugins.UTCTimeSystem = UTCTimeSystem; + plugins.ImportExport = ImportExport; + /** * A tabular view showing the latest values of multiple telemetry points at * once. Formatted so that labels and values are aligned.