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.