From efa037d34be7b1570efdc767c79fad8d4005f10e Mon Sep 17 00:00:00 2001 From: Michael Lando Date: Sun, 19 Feb 2017 12:57:33 +0200 Subject: Add new code new version Change-Id: Ic02a76313503b526f17c3df29eb387a29fe6a42a Signed-off-by: Michael Lando --- .../webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx | 199 +++++ .../lib/ecomp/asdc/sequencer/common/Common.js | 356 ++++++++ .../lib/ecomp/asdc/sequencer/common/Logger.js | 137 ++++ .../lib/ecomp/asdc/sequencer/common/Options.js | 136 ++++ .../components/application/Application.jsx | 268 ++++++ .../asdc/sequencer/components/diagram/Diagram.jsx | 896 +++++++++++++++++++++ .../components/diagram/components/popup/Popup.jsx | 94 +++ .../components/diagram/templates/diagram.html | 56 ++ .../components/diagram/templates/fragment.html | 18 + .../components/diagram/templates/lifeline.html | 19 + .../components/diagram/templates/message.html | 29 + .../components/diagram/templates/occurrence.html | 7 + .../components/diagram/templates/title.html | 3 + .../asdc/sequencer/components/dialog/Dialog.jsx | 222 +++++ .../asdc/sequencer/components/editor/Editor.jsx | 171 ++++ .../editor/components/designer/Designer.jsx | 403 +++++++++ .../designer/components/actions/Actions.jsx | 471 +++++++++++ .../designer/components/lifeline/Lifeline.jsx | 264 ++++++ .../designer/components/lifeline/LifelineNew.jsx | 112 +++ .../designer/components/lifeline/Lifelines.jsx | 136 ++++ .../designer/components/message/Message.jsx | 587 ++++++++++++++ .../designer/components/message/MessageNew.jsx | 106 +++ .../designer/components/message/Messages.jsx | 143 ++++ .../designer/components/metadata/Metadata.jsx | 34 + .../components/editor/components/source/Source.jsx | 86 ++ .../editor/components/toolbar/Toolbar.jsx | 275 +++++++ .../asdc/sequencer/components/export/Export.jsx | 31 + .../ecomp/asdc/sequencer/components/icons/Icon.jsx | 41 + .../asdc/sequencer/components/overlay/Overlay.jsx | 61 ++ .../lib/ecomp/asdc/sequencer/model/Metamodel.js | 94 +++ .../lib/ecomp/asdc/sequencer/model/Metamodels.js | 87 ++ .../webapp/lib/ecomp/asdc/sequencer/model/Model.js | 512 ++++++++++++ .../sequencer/model/demo/scenarios/Scenarios.js | 110 +++ .../model/demo/scenarios/metamodel/BLANK.json | 16 + .../model/demo/scenarios/metamodel/DIMENSIONS.json | 16 + .../model/demo/scenarios/metamodel/ECOMP.json | 62 ++ .../model/demo/scenarios/model/BLANK.json | 37 + .../model/demo/scenarios/model/DIMENSIONS.json | 91 +++ .../model/demo/scenarios/model/ECOMP.json | 514 ++++++++++++ .../model/schema/asdc-sequencer-meta-schema.xsd | 166 ++++ .../model/schema/asdc-sequencer-schema.xsd | 274 +++++++ .../model/schema/asdc_sequencer_meta_schema.json | 332 ++++++++ .../model/schema/asdc_sequencer_schema.json | 582 +++++++++++++ .../model/templates/default.metamodel.json | 17 + .../sequencer/model/templates/default.model.json | 11 + 45 files changed, 8282 insertions(+) create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Common.js create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Logger.js create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Options.js create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodel.js create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodels.js create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Model.js create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/Scenarios.js create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/BLANK.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/DIMENSIONS.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/ECOMP.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/BLANK.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/DIMENSIONS.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/ECOMP.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-meta-schema.xsd create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-schema.xsd create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_meta_schema.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_schema.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.metamodel.json create mode 100644 dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.model.json (limited to 'dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer') diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx new file mode 100644 index 0000000000..ff8e9a22ca --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx @@ -0,0 +1,199 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import React from 'react'; +import Application from './components/application/Application'; +import Common from './common/Common'; +import Options from './common/Options'; +import Model from './model/Model'; +import Metamodel from './model/Metamodel'; +import Metamodels from './model/Metamodels'; +import Scenarios from './model/demo/scenarios/Scenarios'; +import '../../../../res/sdc-sequencer.scss'; +/** + * ASDC Sequencer entry point. + */ +export default class Sequencer extends React.Component { + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + constructor(props, context) { + super(props, context); + + + this.setMetamodel.bind(this); + this.setModel.bind(this); + this.getModel.bind(this); + this.getMetamodel.bind(this); + this.getSVG.bind(this); + this.getDemoScenarios.bind(this); + this.newModel.bind(this); + + // Parse options. + + this.options = new Options(props.options); + + // Default scenarios. + + const scenarios = this.getDemoScenarios(); + this.setMetamodel(scenarios.getMetamodels()); + + // this.setModel(scenarios.getBlank()); + this.setModel(scenarios.getDimensions()); + // this.setModel(scenarios.getECOMP()); + + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Optionally save known metamodels so that subsequent loading and unloading + * of models needn't include the corresponding metamodel. + * @param metamodels array of conformant metamodel JSON definitions. + * @return this. + */ + setMetamodel(metamodels) { + Common.assertType(metamodels, 'Array'); + this.metamodels = new Metamodels(metamodels); + return this; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set current diagram. + * @param modelJSON JSON diagram spec. + * @param metamodelIdOrDefinition optional metamodel definition or reference. Defaults to + * the model's metadata @ref, or the default (permissive) metamodel. + * @return this. + */ + setModel(modelJSON, metamodelIdOrDefinition) { + Common.assertType(modelJSON, 'Object'); + const ref = (modelJSON.metadata) ? modelJSON.metadata.ref : undefined; + const metamodel = this.getMetamodel(metamodelIdOrDefinition || ref); + Common.assertInstanceOf(metamodel, Metamodel); + this.model = new Model(modelJSON, metamodel); + if (this.application) { + this.application.setModel(this.model); + } + return this; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get current diagram state. At any given instant the diagram might not make *sense* + * but it should always be syntactically valid. + * @return current Model. + */ + getModel() { + + if (this.application) { + const model = this.application.getModel(); + if (model) { + return model.unwrap(); + } + } + + return this.model; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Extract SVG element. + * @return stringified SVG element. + */ + getSVG() { + return this.application.getSVG(); + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get demo scenarios, allowing initialization in demo mode from the outside. + * @returns {Scenarios} + */ + getDemoScenarios() { + return new Scenarios(); + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Create new model. + * @param metamodelIdOrDefinition + * @return newly-created model. + */ + newModel(metamodelIdOrDefinition) { + const metamodel = this.getMetamodel(metamodelIdOrDefinition); + Common.assertInstanceOf(metamodel, Metamodel); + const model = new Model({}, metamodel); + if (this.application) { + this.application.setModel(model); + } + return model; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get Metamodel instance corresponding to an ID or JSON definition. + * @param metamodelIdOrDefinition String ID or JSON definition. + * @returns Metamodel instance. + * @private + */ + getMetamodel(metamodelIdOrDefinition) { + const metamodelType = Common.getType(metamodelIdOrDefinition); + if (metamodelType === 'Object') { + return new Metamodel(metamodelIdOrDefinition); + } + return this.metamodels.getMetamodelOrDefault(metamodelIdOrDefinition); + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render current diagram state. + */ + render() { + + if (this.props.model) { + + // If a model was specified as a property, apply it. Otherwise + // fall back to the demo model. + + const scenarios = this.getDemoScenarios(); + const metamodel = [scenarios.getBlankMetamodel(), scenarios.getECOMPMetamodel()]; + if (this.props.metamodel) { + metamodel.push(this.props.metamodel); + } + this.setMetamodel(metamodel); + this.setModel(this.props.model); + } + + return ( + { this.application = a; }} /> + ); + } + +} + +Sequencer.propTypes = { + options: React.PropTypes.object.isRequired, + model: React.PropTypes.object, + metamodel: React.PropTypes.object, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Common.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Common.js new file mode 100644 index 0000000000..7337367dca --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Common.js @@ -0,0 +1,356 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +/** + * Common operations. + */ +export default class Common { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Retrieve and start a simple timer. Retrieve elapsed time by calling #ms(). + * @returns {*} + */ + static timer() { + const start = new Date().getTime(); + return { + ms() { + return (new Date().getTime() - start); + }, + }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get datatype, stripping '[object Boolean]' to just 'Boolean'. + * @param o JS object. + * @return String like String, Number, Date, Null, Undefined, stuff like that. + */ + static getType(o) { + const str = Object.prototype.toString.call(o); + const prefix = '[object '; + if (str.substr(str, prefix.length) === prefix) { + return str.substr(prefix.length, str.length - (prefix.length + 1)); + } + return str; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert that an argument was provided. + * @param value to be checked. + * @param message message on assertion failure. + * @return value. + */ + static assertNotNull(value, message = 'Unexpected null value') { + if (!value) { + throw new Error(message); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument type. + * @param value to be checked. + * @param expected expected type string, e,g. Number from [object Number]. + * @return value. + */ + static assertType(value, expected) { + const type = this.getType(value); + if (type !== expected) { + throw new Error(`Expected type ${expected}, got ${type}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument type. + * @param value to be checked. + * @param unexpected unexpected type string, e,g. Number from [object Number]. + * @return value. + */ + static assertNotType(value, unexpected) { + const type = this.getType(value); + if (type === unexpected) { + throw new Error(`Forbidden type "${unexpected}"`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument is a simple JSON object, and specifically not (something like an) ES6 class. + * @param value to be checked. + * @return value. + */ + static assertPlainObject(value) { + Common.assertType(value, 'Object'); + // TODO + /* + if (!($.isPlainObject(value))) { + throw new Error(`Expected plain object: ${value}`); + } + */ + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument type. + * @param value to be checked. + * @param c expected class. + * @return value. + */ + static assertInstanceOf(value, c) { + Common.assertNotNull(value); + if (!(value instanceof c)) { + throw new Error(`Expected instanceof ${c}: ${value}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert that a string matches a regex. + * @param value value to be tested. + * @param re pattern to be applied. + * @return value. + */ + static assertMatches(value, re) { + this.assertType(value, 'String'); + this.assertType(re, 'RegExp'); + if (!re.test(value)) { + throw new Error(`Value ${value} doesn't match pattern ${re}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert the value of a boolean. + * + * @param bool to be checked. + * @param message optional message on assertion failure. + * @return value. + */ + static assertThat(bool, message) { + if (!bool) { + throw new Error(message || `Unexpected: ${bool}`); + } + return bool; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Verify that a value, generally a function arg, is a DOM element. + * @param value to be checked. + * @return value. + */ + static assertHTMLElement(value) { + if (!Common.isHTMLElement(value)) { + throw new Error(`Expected HTMLElement: ${value}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Check whether a value, generally a function arg, is an HTML DOM element. + * @param o to be checked. + * @return true if DOM element. + */ + static isHTMLElement(o) { + if (typeof HTMLElement === 'object') { + return o instanceof HTMLElement; + } + return o && typeof o === 'object' && o !== null + && o.nodeType === 1 && typeof o.nodeName === 'string'; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Check if a string is non-empty. + * @param s string to be checked. + * @returns false if non-blank string, true otherwise. + */ + static isBlank(s) { + if (Common.getType(s) === 'String') { + return (s.trim().length === 0); + } + return true; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Detect dates that are numbers, milli/seconds since epoch.. + * + * @param n candidate number. + * @returns {boolean} + */ + static isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Parse the text output from a template to a DOM element. + * @param txt input text. + * @returns {Element} + */ + static txt2dom(txt) { + return new DOMParser().parseFromString(txt, 'image/svg+xml').documentElement; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Recursively convert a DOM element to an SVG (namespaced) element. Otherwise + * you get HTML elements that *happen* to have SVG names, but which aren't actually SVG. + * + * @param node DOM node to be converted. + * @param svg to be updated. + * @returns {*} for chaining. + */ + static dom2svg(node, svg) { + + Common.assertNotType(node, 'String'); + + if (node.childNodes && node.childNodes.length > 0) { + + for (const c of node.childNodes) { + switch (c.nodeType) { + case document.TEXT_NODE: + svg.text(c.nodeValue); + break; + default: + break; + } + } + + for (const c of node.childNodes) { + switch (c.nodeType) { + case document.ELEMENT_NODE: + Common.dom2svg(c, svg.append(`svg:${c.nodeName.toLowerCase()}`)); + break; + default: + break; + } + } + } + + if (node.hasAttributes()) { + for (let i = 0; i < node.attributes.length; i++) { + const a = node.attributes.item(i); + svg.attr(a.name, a.value); + } + } + + return svg; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get the lines to be shown in the label. + * + * @param labelText original label text. + * @param wordWrapAt chars at which to break words. + * @param lineWrapAt chars at which to wrap. + * @param maximumLines lines at which to truncate. + * @returns {Array} + */ + static tokenize(labelText = '', wordWrapAt, lineWrapAt, maximumLines) { + + let l = labelText; + + // Hyphenate and break long words. + + const regex = new RegExp(`(\\w{${wordWrapAt - 1}})(?=\\w)`, 'g'); + l = l.replace(regex, '$1- '); + + const labelTokens = l.split(/\s+/); + const lines = []; + let label = ''; + for (const labelToken of labelTokens) { + if (label.length > 0) { + const length = label.length + labelToken.length + 1; + if (length > lineWrapAt) { + lines.push(label.trim()); + label = labelToken; + continue; + } + } + label = `${label} ${labelToken}`; + } + + if (label) { + lines.push(label.trim()); + } + + const truncated = lines.slice(0, maximumLines); + if (truncated.length < lines.length) { + let finalLine = truncated[maximumLines - 1]; + if (finalLine.length > (lineWrapAt - 4)) { + finalLine = finalLine.substring(0, lineWrapAt - 4); + } + finalLine = `${finalLine} ...`; + truncated[maximumLines - 1] = finalLine; + } + + return truncated; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Brutally sanitize an input string. We have no syntax rules, and hence no specific + * rules to apply, but we have very few unconstrained fields, so we can implement a + * crude default and devolve the rest to options. + * @param value value to be sanitized. + * @param options control options including validation rules. + * @param type validation type. + * @returns {*} sanitized string. + * @private + */ + static sanitizeText(value, options, type) { + const rules = Common.assertNotNull(options.validation[type]); + let v = value || rules.defaultValue || ''; + if (rules.replace) { + v = v.replace(rules.replace, ''); + } + if (v.length > rules.maxLength) { + v = `${v.substring(0, rules.maxLength)}...`; + } + return v; + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Logger.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Logger.js new file mode 100644 index 0000000000..187f49bb08 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Logger.js @@ -0,0 +1,137 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +/* eslint-disable no-console */ + +import Common from './Common'; + +/** + * Logger, to allow calls to console.log during development, but + * disable them for production. + */ +export default class Logger { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * No-op call so that we can leave imports in place, + * even when there's no debugging. + */ + static noop() { + // Nothing. + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set debug level. + * @param level threshold. + */ + static setLevel(level) { + this.level = Logger.OFF; + if (Common.getType(level) === 'Number') { + this.level = level; + } else { + this.level = Logger[level]; + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get debug level. + * @returns {number|*} + */ + static getLevel() { + return this.level; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write DEBUG-level log. + * @param msg message or tokens. + */ + static debug(...msg) { + if (this.level >= Logger.DEBUG) { + const out = this.serialize(msg); + console.info(`ASDCS [DEBUG] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write INFO-level log. + * @param msg message or tokens. + */ + static info(...msg) { + if (this.level >= Logger.INFO) { + const out = this.serialize(msg); + console.info(`ASDCS [INFO] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write debug. + * @param msg message or tokens. + */ + static warn(msg) { + if (this.level >= Logger.WARN) { + const out = this.serialize(msg); + console.warn(`ASDCS [WARN] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write error. + * @param msg message or tokens. + */ + static error(...msg) { + if (this.level >= Logger.ERROR) { + const out = this.serialize(msg); + console.error(`ASDCS [ERROR] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Serialize msg. + * @param msg message or tokens. + * @returns {string} + */ + static serialize(...msg) { + let out = ''; + msg.forEach((token) => { + out = `${out}${token}`; + }); + return out; + } +} + +// ///////////////////////////////////////////////////////////////////////////////////////////////// + +Logger.OFF = 0; +Logger.ERROR = 1; +Logger.WARN = 2; +Logger.INFO = 3; +Logger.DEBUG = 4; +Logger.level = Logger.OFF; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Options.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Options.js new file mode 100644 index 0000000000..15897d7ee3 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Options.js @@ -0,0 +1,136 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import _merge from 'lodash/merge'; + +import Logger from './Logger'; + +/** + * A wrapper for an options object. User-supplied options are merged with defaults, + * and the result -- runtime options -- are available by calling #getOptions(). + */ +export default class Options { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct options, applying defaults. + * @param options optional override options. + */ + constructor(options = {}) { + this.options = _merge({}, Options.DEFAULTS, options); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Unwrap options. + * @returns {*} + */ + unwrap() { + return this.options; + } +} + +// ///////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Default options, overridden by anything of the same name. + */ +Options.DEFAULTS = { + log: { + level: Logger.WARN, + }, + demo: false, + useHtmlSelect: true, + diagram: { + svg: { + x: 0, + y: 0, + width: 1600, + height: 1200, + margin: 50, + floodColor: '#009fdb', + scale: { + height: true, + width: true, + minimum: 0.25, + }, + }, + title: { + height: 0, + }, + metadata: false, + lifelines: { + header: { + height: 225, + width: 350, + wrapWords: 14, + wrapLines: 18, + maxLines: 5, + }, + occurrences: { + marginTop: 50, + marginBottom: 75, + foreshortening: 5, + width: 50, + }, + spacing: { + horizontal: 400, + vertical: 400, + }, + }, + messages: { + label: { + wrapWords: 14, + wrapLines: 18, + maxLines: 4, + }, + }, + fragments: { + leftMargin: 150, + topMargin: 200, + widthMargin: 300, + heightMargin: 350, + label: { + wrapWords: 50, + wrapLines: 50, + maxLines: 2, + }, + }, + }, + validation: { + lifeline: { + maxLength: 100, + defaultValue: '', + replace: /[^\-\.\+ &%#@\?\(\)\[\]<>\w\d]/g, + }, + message: { + maxLength: 100, + defaultValue: '', + replace: /[^\-\.\+ &%#@\?\(\)\[\]<>\w\d]/g, + }, + notes: { + maxLength: 255, + defaultValue: '', + }, + guard: { + maxLength: 80, + defaultValue: '', + replace: /[^\-\.\+ &%#@\?\(\)\[\]<>\w\d]/g, + }, + }, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx new file mode 100644 index 0000000000..20b06922c8 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx @@ -0,0 +1,268 @@ + +import React from 'react'; + +import Common from '../../common/Common'; +import Logger from '../../common/Logger'; +import Diagram from '../diagram/Diagram'; +import Dialog from '../dialog/Dialog'; +import Editor from '../editor/Editor'; +import Export from '../export/Export'; +import Overlay from '../overlay/Overlay'; + +/** + * Application controller, also a view. + */ +export default class Application extends React.Component { + + /** + * Construct application view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + + this.sequencer = Common.assertNotNull(props.sequencer); + this.model = this.sequencer.getModel(); + this.metamodel = this.sequencer.getMetamodel(); + this.options = props.options; + Logger.setLevel(this.options.unwrap().log.level); + + // Bindings. + + this.showInfoDialog = this.showInfoDialog.bind(this); + this.showEditDialog = this.showEditDialog.bind(this); + this.showConfirmDialog = this.showConfirmDialog.bind(this); + this.hideOverlay = this.hideOverlay.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get application options. + * @returns JSON options, see Options.js. + */ + getOptions() { + return this.options.unwrap(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set diagram name. + * @param n diagram (human-readable) name. + */ + setName(n) { + this.diagram.setName(n); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set diagram model. + * @param model diagram instance. + */ + setModel(model) { + + Common.assertNotNull(model); + + this.model = model; + + if (this.editor) { + this.editor.render(); + } + + if (this.diagram) { + this.diagram.render(); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get Model wrapper. + * @returns Model. + */ + getModel() { + return this.model; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get SVG element. + * @returns {*} + */ + getSVG() { + return this.diagram.getSVG(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get top-level widget. Provides the demo toolbar with access to the public API. + * @returns {*} + */ + getSequencer() { + return this.sequencer; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present info dialog. + * @param msg info message. + */ + showInfoDialog(msg) { + this.dialog.showInfoDialog(msg); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present error dialog. + * @param msg error message. + */ + showErrorDialog(msg) { + this.dialog.showErrorDialog(msg); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present confirmation dialog. + * @param msg info message. + * @param cb callback function to be invoked on OK. + */ + showConfirmDialog(msg, cb) { + Common.assertType(cb, 'Function'); + this.dialog.showConfirmDialog(msg, cb); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Present edit (textarea) dialog. + * @param msg prompt. + * @param text current edit text. + * @param cb callback function to be invoked on OK, taking the updated text + * as an argument. + */ + showEditDialog(msg, text, cb) { + this.dialog.showEditDialog(msg, text, cb); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select lifeline by ID. + * @param id lifeline ID. + */ + selectLifeline(id) { + if (this.editor) { + this.editor.selectLifeline(id); + } + if (this.diagram) { + this.diagram.selectLifeline(id); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Select message by ID. + * @param id message ID. + */ + selectMessage(id) { + if (this.editor) { + this.editor.selectMessage(id); + } + if (this.diagram) { + this.diagram.selectMessage(id); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * (Re)render just the diagram. + */ + renderDiagram() { + this.diagram.redraw(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show overlay between application and modal dialog. + */ + showOverlay() { + if (this.overlay) { + this.overlay.setVisible(true); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Hide overlay between application and modal dialog. + */ + hideOverlay() { + if (this.overlay) { + this.overlay.setVisible(false); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Capture mouse move events, for resize. + * @param event move event. + */ + onMouseMove(event) { + if (this.editor) { + this.editor.onMouseMove(event); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Propagate mouse event to the editor that manages the resize. + */ + onMouseUp() { + if (this.editor) { + this.editor.onMouseUp(); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render current model state. + */ + render() { + + return ( + +
+ + { this.editor = r; }} /> + { this.diagram = r; }} /> + { this.dialog = r; }} /> + + { this.overlay = r; }} /> + +
+ ); + } + +} + +/** React properties. */ +Application.propTypes = { + options: React.PropTypes.object.isRequired, + sequencer: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx new file mode 100644 index 0000000000..f2da7a5a1b --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx @@ -0,0 +1,896 @@ + +import React from 'react'; +import _template from 'lodash/template'; +import _merge from 'lodash/merge'; +import d3 from 'd3'; + +import Common from '../../common/Common'; +import Logger from '../../common/Logger'; +import Popup from './components/popup/Popup'; + +/** + * SVG diagram view. + */ +export default class Diagram extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct React view. + * @param props properties. + * @param context context. + */ + constructor(props, context) { + super(props, context); + + this.application = Common.assertNotNull(props.application); + this.options = this.application.getOptions().diagram; + + this.events = {}; + this.state = { + height: 0, + width: 0, + }; + + this.templates = { + diagram: _template(require('./templates/diagram.html')), + lifeline: _template(require('./templates/lifeline.html')), + message: _template(require('./templates/message.html')), + occurrence: _template(require('./templates/occurrence.html')), + fragment: _template(require('./templates/fragment.html')), + title: _template(require('./templates/title.html')), + }; + + this.handleResize = this.handleResize.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set diagram name. + * @param n name. + */ + setName(n) { + this.svg.select('').text(n); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get SVG from diagram. + * @returns {*|string} + */ + getSVG() { + const svg = this.svg.node().outerHTML; + return svg.replace(' 0) ? `height:${titleHeight}` : 'asdcs-hidden'; + + return ( +
+
{name}
+
{ this.wrapper = r; }}>
+ { this.popup = r; }} /> +
+ ); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + redraw() { + this.updateSVG(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initial render. + */ + componentDidMount() { + window.addEventListener('resize', this.handleResize); + this.updateSVG(); + + // Insurance: + + setTimeout(() => { + this.handleResize(); + }, 500); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render on update. + */ + componentDidUpdate() { + this.updateSVG(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Redraw SVG diagram. So far it's fast enough that it doesn't seem to matter whether + * it's completely redrawn. + */ + updateSVG() { + + if (!this.svg) { + const svgparams = _merge({}, this.options.svg); + this.wrapper.innerHTML = this.templates.diagram(svgparams); + this.svg = d3.select(this.wrapper).select('svg'); + } + + if (this.state.height === 0) { + + // We'll get a resize event, and the height will be non-zero when it's actually time. + + return; + } + + if (this.state.height && this.state.width) { + const margin = this.options.svg.margin; + const x = -margin; + const y = -margin; + const height = this.state.height + (margin * 2); + const width = this.state.width + (margin * 2); + const viewBox = `${x} ${y} ${width} ${height}`; + this.svg.attr('viewBox', viewBox); + } + + + // If we've already rendered, then save the current scale/translate so that we + // can reapply it after rendering. + + const gContentSelection = this.svg.selectAll('g.asdcs-diagram-content'); + if (gContentSelection.size() === 1) { + const transform = gContentSelection.attr('transform'); + if (transform) { + this.savedTransform = transform; + } + } + + // Empty the document. We're starting again. + + this.svg.selectAll('.asdcs-diagram-content').remove(); + + // Extract the model. + + const model = this.application.getModel(); + if (!model) { + return; + } + const modelJSON = model.unwrap(); + + // Extract dimension options. + + const header = this.options.lifelines.header; + const spacing = this.options.lifelines.spacing; + + // Make separate container elements so that we can control Z order. + + const gContent = this.svg.append('g').attr('class', 'asdcs-diagram-content'); + const gLifelines = gContent.append('g').attr('class', 'asdcs-diagram-lifelines'); + const gCanvas = gContent.append('g').attr('class', 'asdcs-diagram-canvas'); + gCanvas.append('g').attr('class', 'asdcs-diagram-occurrences'); + gCanvas.append('g').attr('class', 'asdcs-diagram-fragments'); + gCanvas.append('g').attr('class', 'asdcs-diagram-messages'); + + // Lifelines ----------------------------------------------------------------------------------- + + const actorsById = {}; + const positionsByMessageId = {}; + const lifelines = []; + for (const actor of modelJSON.diagram.lifelines) { + const x = (header.width / 2) + (lifelines.length * spacing.horizontal); + Diagram._processLifeline(actor, x); + lifelines.push({ x, actor }); + actorsById[actor.id] = actor; + } + + // Messages ------------------------------------------------------------------------------------ + + // Analyze occurrence information. + + const occurrences = model.analyzeOccurrences(); + const fragments = model.analyzeFragments(); + let y = this.options.lifelines.header.height + spacing.vertical; + let messageIndex = 0; + for (const step of modelJSON.diagram.steps) { + if (step.message) { + positionsByMessageId[step.message.id] = positionsByMessageId[step.message.id] || {}; + positionsByMessageId[step.message.id].y = y; + this._drawMessage(gCanvas, step.message, y, actorsById, + positionsByMessageId, ++messageIndex, occurrences, fragments); + } + y += spacing.vertical; + } + + // --------------------------------------------------------------------------------------------- + + // Draw the actual (dashed) lifelines in a background . + + this._drawLifelines(gLifelines, lifelines, y); + + // Initialize mouse event handlers. + + this._initMouseEvents(gLifelines, gCanvas); + + // Scale to fit. + + const bb = gContent.node().getBBox(); + this._initZoom(gContent, bb.width, bb.height); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw message into SVG canvas. + * @param gCanvas container. + * @param message message to be rendered. + * @param y current y position. + * @param actorsById actor lookup. + * @param positionsByMessageId x- and y-position of each message. + * @param messageIndex where we are in the set of messages to be rendered. + * @param oData occurrences info. + * @param fData fragments info. + * @private + */ + _drawMessage(gCanvas, message, y, actorsById, positionsByMessageId, + messageIndex, oData, fData) { + + Common.assertNotNull(oData); + + const request = message.type === 'request'; + const fromActor = request ? actorsById[message.from] : actorsById[message.to]; + const toActor = request ? actorsById[message.to] : actorsById[message.from]; + + if (!fromActor) { + Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'from' not found.`); + return; + } + + if (!toActor) { + Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'to' not found.`); + return; + } + + // Occurrences. -------------------------------------------------------------------------------- + + if (message.occurrence) { + Logger.debug(`Found occurrence for ${message.name}: ${JSON.stringify(message.occurrence)}`); + } + const activeTo = Diagram._calcActive(oData, toActor.id); + this._drawOccurrence(gCanvas, oData, positionsByMessageId, fromActor, message.id); + this._drawOccurrence(gCanvas, oData, positionsByMessageId, toActor, message.id); + const activeFrom = Diagram._calcActive(oData, fromActor.id); + + // Messages. ----------------------------------------------------------------------------------- + + const gMessages = gCanvas.select('g.asdcs-diagram-messages'); + + // Save positions for later. + + const positions = positionsByMessageId[message.id]; + positions.x0 = fromActor.x; + positions.x1 = toActor.x; + + // Calculate. + + const leftToRight = fromActor.x < toActor.x; + const loopback = (message.to === message.from); + const x1 = this._calcMessageX(activeTo, toActor.x, true, leftToRight); + const x0 = loopback ? x1 : this._calcMessageX(activeFrom, fromActor.x, false, leftToRight); + + let messagePath; + if (loopback) { + + // To self. + + messagePath = `M${x1},${y}`; + messagePath = `${messagePath} L${x1 + 200},${y}`; + messagePath = `${messagePath} L${x1 + 200},${y + 50}`; + messagePath = `${messagePath} L${x1},${y + 50}`; + } else { + + // Between lifelines. + + messagePath = `M${x0},${y}`; + messagePath = `${messagePath} L${x1},${y}`; + } + + const styles = Diagram._getMessageStyles(message); + + // Split message over lines. + + const messageWithPrefix = `${messageIndex}. ${message.name}`; + const maxLines = this.options.messages.label.maxLines; + const wrapWords = this.options.messages.label.wrapWords; + const wrapLines = this.options.messages.label.wrapLines; + const messageLines = Common.tokenize(messageWithPrefix, wrapWords, wrapLines, maxLines); + + const messageTxt = this.templates.message({ + id: message.id, + classes: styles.css, + marker: styles.marker, + dasharray: styles.dasharray, + labels: messageLines, + lines: maxLines, + path: messagePath, + index: messageIndex, + x0, x1, y, + }); + + const messageEl = Common.txt2dom(messageTxt); + const gMessage = gMessages.append('g'); + Common.dom2svg(messageEl, gMessage); + + // Set the background's bounding box to that of the text, + // so that they fit snugly. + + const labelBB = gMessage.select('.asdcs-diagram-message-label').node().getBBox(); + gMessage.select('.asdcs-diagram-message-label-bg') + .attr('x', labelBB.x) + .attr('y', labelBB.y) + .attr('height', labelBB.height) + .attr('width', labelBB.width); + + // Fragments. ---------------------------------------------------------------------------------- + + const fragment = fData[message.id]; + if (fragment) { + + // It ends on this message. + + this._drawFragment(gCanvas, fragment, positionsByMessageId); + + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw a single occurrence. + * @param gCanvas container. + * @param oData occurrence data. + * @param positionsByMessageId map of y positions by message ID. + * @param actor wrapper containing lifeline ID (.id), position (.x) and name (.name). + * @param messageId message identifier. + * @private + */ + _drawOccurrence(gCanvas, oData, positionsByMessageId, actor, messageId) { + + Common.assertType(oData, 'Object'); + Common.assertType(positionsByMessageId, 'Object'); + Common.assertType(actor, 'Object'); + Common.assertType(messageId, 'String'); + + const gOccurrences = gCanvas.select('g.asdcs-diagram-occurrences'); + + const oOptions = this.options.lifelines.occurrences; + const oWidth = oOptions.width; + const oHalfWidth = oWidth / 2; + const oForeshortening = oOptions.foreshortening; + const oMarginTop = oOptions.marginTop; + const oMarginBottom = oOptions.marginBottom; + const o = oData[actor.id]; + + const active = Diagram._calcActive(oData, actor.id); + + const x = (actor.x - oHalfWidth) + (active * oWidth); + const positions = positionsByMessageId[messageId]; + const y = positions.y; + + let draw = true; + if (o) { + + if (o.start[messageId]) { + + // Starting, but drawing nothing until we find the end. + + o.active.push(messageId); + draw = false; + + } else if (active > 0) { + + const startMessageId = o.stop[messageId]; + if (startMessageId) { + + // OK, it ends here. Draw the occurrence box. + + o.active.pop(); + const foreshorteningY = active * oForeshortening; + const startY = positionsByMessageId[startMessageId].y; + const height = ((oMarginTop + oMarginBottom) + (y - startY)) - (foreshorteningY * 2); + const oProps = { + x: (actor.x - oHalfWidth) + ((active - 1) * oWidth), + y: ((startY - oMarginTop) + foreshorteningY), + height, + width: oWidth, + }; + + const occurrenceTxt = this.templates.occurrence(oProps); + const occurrenceEl = Common.txt2dom(occurrenceTxt); + Common.dom2svg(occurrenceEl, gOccurrences.append('g')); + + } + draw = false; + } + } + + if (draw) { + + // Seems this is a singleton occurrence. We just draw a wee box around it. + + const height = (oMarginTop + oMarginBottom); + const occurrenceProperties = { x, y: y - oMarginTop, height, width: oWidth }; + const defaultTxt = this.templates.occurrence(occurrenceProperties); + const defaultEl = Common.txt2dom(defaultTxt); + Common.dom2svg(defaultEl, gOccurrences.append('g')); + } + + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw box(es) around fragment(s). + * @param gCanvas container. + * @param fragment fragment definition, corresponding to its final (stop) message. + * @param positionsByMessageId message dimensions. + * @private + */ + _drawFragment(gCanvas, fragment, positionsByMessageId) { + + const optFragments = this.options.fragments; + const gFragments = gCanvas.select('g.asdcs-diagram-fragments'); + const p1 = positionsByMessageId[fragment.stop]; + if (p1 && fragment.start && fragment.start.length > 0) { + + for (const start of fragment.start) { + + const message = this.application.getModel().getMessageById(start); + const bounds = this._calcFragmentBounds(message, fragment, positionsByMessageId); + if (bounds) { + + const maxLines = this.options.fragments.label.maxLines; + const wrapWords = this.options.fragments.label.wrapWords; + const wrapLines = this.options.fragments.label.wrapLines; + const lines = Common.tokenize(message.fragment.guard, wrapWords, wrapLines, maxLines); + + const params = { + id: start, + x: bounds.x0 - optFragments.leftMargin, + y: bounds.y0 - optFragments.topMargin, + height: (bounds.y1 - bounds.y0) + optFragments.heightMargin, + width: (bounds.x1 - bounds.x0) + optFragments.widthMargin, + operator: (message.fragment.operator || 'alt'), + lines, + }; + + const fragmentTxt = this.templates.fragment(params); + const fragmentEl = Common.txt2dom(fragmentTxt); + const gFragment = gFragments.append('g'); + Common.dom2svg(fragmentEl, gFragment); + + const labelBB = gFragment.select('.asdcs-diagram-fragment-guard').node().getBBox(); + gFragment.select('.asdcs-diagram-fragment-guard-bg') + .attr('x', labelBB.x) + .attr('y', labelBB.y) + .attr('height', labelBB.height) + .attr('width', labelBB.width); + + } else { + Logger.warn(`Bad fragment: ${JSON.stringify(fragment)}`); + } + } + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + _calcFragmentBounds(startMessage, fragment, positionsByMessageId) { + if (startMessage) { + const steps = this.application.getModel().unwrap().diagram.steps; + const bounds = { x0: 99999, x1: 0, y0: 99999, y1: 0 }; + let foundStart = false; + let foundStop = false; + for (const step of steps) { + const message = step.message; + if (message) { + if (message.id === startMessage.id) { + foundStart = true; + } + if (foundStart && !foundStop) { + const positions = positionsByMessageId[message.id]; + if (positions) { + bounds.x0 = Math.min(bounds.x0, Math.min(positions.x0, positions.x1)); + bounds.y0 = Math.min(bounds.y0, positions.y); + bounds.x1 = Math.max(bounds.x1, Math.max(positions.x0, positions.x1)); + bounds.y1 = Math.max(bounds.y1, positions.y); + } else { + // This probably means it hasn't been recorded yet, which is fine, because + // we draw fragments from where they END. + foundStop = true; + } + } + + if (message.id === fragment.stop) { + foundStop = true; + } + } + } + return bounds; + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Draw all lifelines. + * @param gLifelines lifelines container. + * @param lifelines lifelines definitions. + * @param y height. + * @private + */ + _drawLifelines(gLifelines, lifelines, y) { + + const maxLines = this.options.lifelines.header.maxLines; + const wrapWords = this.options.lifelines.header.wrapWords; + const wrapLines = this.options.lifelines.header.wrapLines; + + for (const lifeline of lifelines) { + const lines = Common.tokenize(lifeline.actor.name, wrapWords, wrapLines, maxLines); + const lifelineTxt = this.templates.lifeline({ + x: lifeline.x, + y0: 0, + y1: y, + lines, + rows: maxLines, + headerHeight: this.options.lifelines.header.height, + headerWidth: this.options.lifelines.header.width, + id: lifeline.actor.id, + }); + + const lifelineEl = Common.txt2dom(lifelineTxt); + Common.dom2svg(lifelineEl, gLifelines.append('g')); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initialize all mouse events. + * @param gLifelines lifelines container. + * @param gCanvas top-level canvas container. + * @private + */ + _initMouseEvents(gLifelines, gCanvas) { + + const self = this; + const source = 'asdcs'; + const origin = `${window.location.protocol}//${window.location.host}`; + + let timer; + gLifelines.selectAll('.asdcs-diagram-lifeline-selectable') + .on('mouseenter', function f() { + timer = setTimeout(() => { + self.application.selectLifeline(d3.select(this.parentNode).attr('data-id')); + }, 150); + }) + .on('mouseleave', () => { + clearTimeout(timer); + self.application.selectLifeline(); + }) + .on('click', function f() { + const id = d3.select(this.parentNode).attr('data-id'); + window.postMessage({ source, id, type: 'lifeline' }, origin); + }); + + gLifelines.selectAll('.asdcs-diagram-lifeline-heading-box') + .on('mouseenter', function f() { + timer = setTimeout(() => { + self.application.selectLifeline(d3.select(this.parentNode).attr('data-id')); + }, 150); + }) + .on('mouseleave', () => { + clearTimeout(timer); + self.application.selectLifeline(); + }) + .on('click', function f() { + const id = d3.select(this.parentNode).attr('data-id'); + window.postMessage({ source, id, type: 'lifelineHeader' }, origin); + }); + + gCanvas.selectAll('.asdcs-diagram-message-selectable') + .on('mouseenter', function f() { + self.events.message = { x: d3.event.pageX, y: d3.event.pageY }; + timer = setTimeout(() => { + self.application.selectMessage(d3.select(this.parentNode).attr('data-id')); + }, 200); + }) + .on('mouseleave', () => { + delete self.events.message; + clearTimeout(timer); + self.application.selectMessage(); + }) + .on('click', function f() { + const id = d3.select(this.parentNode).attr('data-id'); + window.postMessage({ source, id, type: 'message' }, origin); + }); + + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get CSS classes to be applied to a message, according to whether request/response + * or synchronous/asynchronous. + * @param message message being rendered. + * @returns CSS class name(s). + * @private + */ + static _getMessageStyles(message) { + + let marker = 'asdcsDiagramArrowSolid'; + let dasharray = ''; + let css = 'asdcs-diagram-message'; + if (message.type === 'request') { + css = `${css} asdcs-diagram-message-request`; + } else { + css = `${css} asdcs-diagram-message-response`; + marker = 'asdcsDiagramArrowOpen'; + dasharray = '30, 10'; + } + + if (message.asynchronous) { + css = `${css} asdcs-diagram-message-asynchronous`; + marker = 'asdcsDiagramArrowOpen'; + } else { + css = `${css} asdcs-diagram-message-synchronous`; + } + + return { css, marker, dasharray }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initialize or reinitialize zoom. This sets the initial zoom in the case of + * a re-rendering, and initializes the eventhandling in all cases. + * + * It does some fairly risky parsing of the 'transform' attribute, assuming that it + * can contain scale() and translate(). But only the zoom handler and us are writing + * the transform values, so that's probably OK. + * + * @param gContent container. + * @param width diagram width. + * @param height diagram height. + * @private + */ + _initZoom(gContent, width, height) { + + const zoomed = function zoomed() { + gContent.attr('transform', + `translate(${d3.event.translate})scale(${d3.event.scale})`); + }; + + const viewWidth = this.state.width || this.options.svg.width; + const viewHeight = this.state.height || this.options.svg.height; + const scaleMinimum = this.options.svg.scale.minimum; + const scaleWidth = viewWidth / width; + const scaleHeight = viewHeight / height; + + let scale = scaleMinimum; + if (this.options.svg.scale.width) { + scale = Math.max(scale, scaleWidth); + } + if (this.options.svg.scale.height) { + scale = Math.min(scale, scaleHeight); + } + + scale = Math.max(scale, scaleMinimum); + + let translate = [0, 0]; + if (this.savedTransform) { + const s = this.savedTransform; + const scaleStart = s.indexOf('scale('); + if (scaleStart !== -1) { + scale = parseFloat(s.substring(scaleStart + 6, s.length - 1)); + } + const translateStart = s.indexOf('translate('); + if (translateStart !== -1) { + const spec = s.substring(translateStart + 10, s.indexOf(')', translateStart)); + const tokens = spec.split(','); + translate = [parseFloat(tokens[0]), parseFloat(tokens[1])]; + } + + gContent.attr('transform', this.savedTransform); + } else { + gContent.attr('transform', `scale(${scale})`); + } + + const zoom = d3.behavior.zoom() + .scale(scale) + .scaleExtent([scaleMinimum, 10]) + .translate(translate) + .on('zoom', zoomed); + this.svg.call(zoom); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Hide from the linter the fact that we're modifying the lifeline. + * @param lifeline to be updated with X position. + * @param x X position. + * @private + */ + static _processLifeline(lifeline, x) { + const actor = lifeline; + actor.id = actor.id || actor.name; + actor.x = x; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Derive active occurrences for lifeline. + * @param oData occurrences data. + * @param lifelineId lifeline to be analyzed. + * @returns {number} + * @private + */ + static _calcActive(oData, lifelineId) { + const o = oData[lifelineId]; + let active = 0; + if (o && o.active) { + active = o.active.length; + } + return active; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Derive the X position of an occurrence on a lifeline, taking into account how + * many occurrences are active. + * @param active active count. + * @param x lifeline X position; basis for offset. + * @param arrow whether this is the arrow (to) end. + * @param leftToRight whether this message goes left-to-right. + * @returns {*} calculated X position for occurrence left-hand side. + * @private + */ + _calcMessageX(active, x, arrow, leftToRight) { + const width = this.options.lifelines.occurrences.width; + const halfWidth = width / 2; + const active0 = Math.max(0, active - 1); + let calculated = x + (active0 * width); + if (arrow) { + // End (ARROW). + if (leftToRight) { + calculated -= halfWidth; + } else { + calculated += halfWidth; + } + } else { + // Start (NOT ARROW). + if (leftToRight) { + calculated += halfWidth; + } else { + calculated -= halfWidth; + } + } + + return calculated; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show popup upon hovering over a messages that has associated notes. + * @param id + * @private + */ + _showNotesPopup(id) { + if (this.popup) { + if (id) { + const message = this.application.getModel().getMessageById(id); + if (message && message.notes && message.notes.length > 0 && this.events.message) { + this.popup.setState({ + visible: true, + left: this.events.message.x - 50, + top: this.events.message.y + 20, + notes: message.notes[0], + }); + } + } else { + this.popup.setState({ visible: false, notes: '' }); + } + } + } +} + + +Diagram.propTypes = { + application: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx new file mode 100644 index 0000000000..08c6da1e76 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx @@ -0,0 +1,94 @@ + + +import React from 'react'; + +import Icon from '../../../icons/Icon'; +import iconEdit from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/edit.svg'; + +/** + * A hover-over popup. It shows notes, but perhaps will be put to other uses. + * @param props React properties. + * @returns {XML} + * @constructor + */ +export default class Popup extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct react view. + * @param props element properties (of which there are none). + * @param context react context. + */ + constructor(props, context) { + super(props, context); + this.state = { + top: 0, + left: 0, + visible: false, + notes: '', + }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render view. + * @returns {XML} + */ + render() { + + // Build CSS + styles to position and configure popup. + + let top = this.state.top; + let left = this.state.left; + + const popupHeight = 200; + const popupWidth = 320; + + let auxCssVertical = 'top'; + let auxCssHorizontal = 'left'; + + if (this.state.top > (window.innerHeight - popupHeight)) { + top -= (popupHeight + 50); + auxCssVertical = 'bottom'; + } + + if (this.state.left > (window.innerWidth - popupWidth)) { + left -= (popupWidth - 80); + auxCssHorizontal = 'right'; + } + + const auxCss = `asdcs-diagram-popup-${auxCssVertical}${auxCssHorizontal}`; + const styles = { + top, + left, + display: (this.state.visible ? 'block' : 'none'), + }; + + // Render element. + + let notes = this.state.notes || ''; + if (notes.length > 255) { + notes = notes.substring(0, 255); + notes = `${notes} ...`; + } + + return ( +
+
Notes
+
+
+ +
+
+
+ {notes} +
+
+
+
+
+ ); + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html new file mode 100644 index 0000000000..22893ce864 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html new file mode 100644 index 0000000000..812f5fcfb8 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html @@ -0,0 +1,18 @@ + + + + + + + <%-operator%> + + + + <% + for (var lineIndex = 0; lineIndex < lines.length ; lineIndex++) { + %><%- lines[lineIndex] %><% + }%> + + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html new file mode 100644 index 0000000000..cd01d42c5a --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html @@ -0,0 +1,19 @@ + + + + + <% + for (var linesIndex = 0; linesIndex < lines.length && linesIndex < rows ; linesIndex++) { + %><%- lines[linesIndex] %><% + } + %> + + + + + + + + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html new file mode 100644 index 0000000000..bd4c33a016 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html @@ -0,0 +1,29 @@ + + + <% + var delta = 40; + var x = (x0 + x1) / 2; + var y0 = y - ((labels.length + 1) * delta); + %> + + + + <% + for (var labelIndex = 0; labelIndex < labels.length && labelIndex < lines ; labelIndex++) { + %><%- labels[labelIndex] %><% + }%> + + + + + + + + + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html new file mode 100644 index 0000000000..0af9ff3d68 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html @@ -0,0 +1,7 @@ + + + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html new file mode 100644 index 0000000000..b7a5d68a6d --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html @@ -0,0 +1,3 @@ + + <%-title%> + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx new file mode 100644 index 0000000000..4429d80bc6 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx @@ -0,0 +1,222 @@ + + +import React from 'react'; + +import Icon from '../icons/Icon'; +import iconQuestion from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/question.svg'; +import iconExclaim from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/exclaim.svg'; +import iconInfo from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/info.svg'; +import iconEdit from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/edit.svg'; +import iconClose from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/close.svg'; + +/** + * Multi-purpose dialog. Rendered into the page on initialization, and then + * configured, shown and hidden as required. + */ +export default class Dialog extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + */ + constructor(props, context) { + + super(props, context); + + this.MODE = { + INFO: { + icon: 'asdcs-icon-info', + heading: 'Information', + }, + ERROR: { + icon: 'asdcs-icon-exclaim', + heading: 'Error', + }, + EDIT: { + icon: 'asdcs-icon-edit', + heading: 'Edit', + edit: true, + confirm: true, + }, + CONFIRM: { + icon: 'asdcs-icon-question', + heading: 'Confirm', + confirm: true, + }, + }; + + this.state = { + mode: this.MODE.INFO, + message: '', + text: '', + visible: false, + }; + + // Bindings. + + this.onClickOK = this.onClickOK.bind(this); + this.onClickCancel = this.onClickCancel.bind(this); + this.onChangeText = this.onChangeText.bind(this); + this.showConfirmDialog = this.showConfirmDialog.bind(this); + this.showInfoDialog = this.showInfoDialog.bind(this); + this.showEditDialog = this.showEditDialog.bind(this); + this.showErrorDialog = this.showErrorDialog.bind(this); + this.showDialog = this.showDialog.bind(this); + + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show info dialog. + * @param message info message. + */ + showInfoDialog(message) { + this.showDialog(this.MODE.INFO, { message }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show error dialog. + * @param message error message. + */ + showErrorDialog(message) { + this.showDialog(this.MODE.ERROR, { message }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show edit dialog. + * @param message dialog message. + * @param text current edit text. + * @param callback callback function to be invoked on OK. + */ + showEditDialog(message, text, callback) { + this.showDialog(this.MODE.EDIT, { message, text, callback }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show confirmation dialog. + * @param message dialog message. + * @param callback callback function to be invoked on OK. + */ + showConfirmDialog(message, callback) { + this.showDialog(this.MODE.CONFIRM, { message, callback }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle buttonclick. + */ + onClickOK() { + this.props.application.hideOverlay(); + this.setState({ visible: false }); + if (this.callback) { + + // So far the only thing we can return is edit text, but send it back + // as properties to allow for future return values. + + this.callback({ text: this.state.text }); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle buttonclick. + */ + onClickCancel() { + this.props.application.hideOverlay(); + this.setState({ visible: false }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Handle text changes. + * @param event update event. + */ + onChangeText(event) { + this.setState({ text: event.target.value }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Show dialog in specified configuration. + * @param mode dialog mode. + * @param args dialog parameters, varying slightly by dialog type. + * @private + */ + showDialog(mode, args) { + this.props.application.showOverlay(); + this.callback = args.callback; + this.setState({ + mode, + visible: true, + message: args.message || '', + text: args.text || '', + }); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render dialog into the page, initially hidden. + */ + render() { + + const dialogClass = (this.state.visible) ? '' : 'asdcs-hidden'; + const cancelClass = (this.callback) ? '' : 'asdcs-hidden'; + const textClass = (this.state.mode === this.MODE.EDIT) ? '' : 'asdcs-hidden'; + + return ( +
+
{this.state.mode.heading}
+
+ +
+
+ + + + +
+
+ {this.state.message} +
+
+ +
+ ); + } +} + +Source.propTypes = { + application: React.PropTypes.object.isRequired, +}; + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx new file mode 100644 index 0000000000..dd75180b2a --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx @@ -0,0 +1,275 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import React from 'react'; + +import Common from '../../../../common/Common'; + +import iconPlus from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/plus.svg'; +import iconOpen from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/open.svg'; + +/** + * Toolbar view. Buttons offered in the toolbar depend on the mode. Unless in demo mode, + * all you get are the buttons for toggling between JSON/YAML/Designer. + */ +export default class Toolbar extends React.Component { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct view. + */ + constructor(props, context) { + super(props, context); + this.application = Common.assertType(this.props.application, 'Object'); + this.editor = Common.assertType(this.props.editor, 'Object'); + this.mode = 'design'; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set editor mode, one of {design, json, yaml}. + * @param mode + */ + setMode(mode = 'design') { + this.mode = mode; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render into the DOM. + */ + render() { + + const demo = this.application.getOptions().demo; + const demoCss = demo ? '' : 'asdc-hide'; + + return ( +
+
+ + + + + + +
+
+ + + +
+
+ ); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Initialize eventhandlers. + * @private + * + _initEvents() { + + $('button.asdcs-button-open', this.$el).click(() => { + this._doDemoOpen(); + }); + + $('button.asdcs-button-new', this.$el).click(() => { + this._doDemoNew(); + }); + + $('button.asdcs-button-save', this.$el).click(() => { + this._doDemoSave(); + }); + + $('button.asdcs-button-upload', this.$el).click(() => { + this._doDemoUpload(); + }); + + $('button.asdcs-button-download', this.$el).click(() => { + this._doDemoDownload(); + }); + + $('button.asdcs-button-validate', this.$el).click(() => { + this._doDemoValidate(); + }); + + $('button.asdcs-button-json', this.$el).click((e) => { + if ($(e.target).hasClass('asdcs-active')) { + return; + } + this.editor.toggleToJSON(); + }); + + $('button.asdcs-button-yaml', this.$el).click((e) => { + if ($(e.target).hasClass('asdcs-active')) { + return; + } + this.editor.toggleToYAML(); + }); + + $('button.asdcs-button-design', this.$el).click((e) => { + if ($(e.target).hasClass('asdcs-active')) { + return; + } + this.editor.toggleToDesign(); + }); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoOpen() { + const complete = function complete() { + const sequencer = this.application.getSequencer(); + const scenarios = sequencer.getDemoScenarios(); + sequencer.setModel(scenarios.getECOMP()); + }; + this.application.showConfirmDialog('[DEMO MODE] Open a canned DEMO sequence ' + + 'via the public #setModel() API?', complete); + + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoNew() { + const complete = function complete() { + const sequencer = this.application.getSequencer(); + sequencer.newModel(); + }; + this.application.showConfirmDialog('[DEMO MODE] Create an empty sequence via the ' + + 'public #newModel() API?', complete); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoSave() { + const sequencer = this.application.getSequencer(); + Logger.info(`[DEMO MODE] model:\n${JSON.stringify(sequencer.getModel(), null, 4)}`); + this.application.showInfoDialog('[DEMO MODE] Retrieved model via the public #getModel ' + + 'API and logged its JSON to the console.'); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoUpload() { + const sequencer = this.application.getSequencer(); + const svg = sequencer.getSVG(); + // console.log(`[DEMO MODE] SVG:\n${svg}`); + const $control = this.$el.closest('.asdcs-control'); + Logger.info(`parent: ${$control.length}`); + const $form = $('form.asdcs-export', $control); + Logger.info(`form: ${$form.length}`); + $('input[name=svg]', $form).val(svg); + try { + $form.submit(); + } catch (e) { + Logger.error(e); + this.application.showErrorDialog('[DEMO MODE] Export service not available. Retrieved ' + + 'SVG via the public #getSVG API and dumped it to the console.'); + } + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoDownload() { + const json = JSON.stringify(this.application.getSequencer().getModel()); + const $control = this.$el.closest('.asdcs-control'); + const $a = $('').appendTo($control); + $a.attr('href', `data:application/json;charset=utf-8,${encodeURIComponent(json)}`); + $a[0].click(); + $a.remove(); + } + */ + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Demo action. + * + _doDemoValidate() { + this.application.showInfoDialog('[DEMO MODE] Dumping validation result to the console.'); + const errors = this.application.getModel().validate(); + Logger.info(`[DEMO MODE] Validation: ${JSON.stringify(errors, null, 4)}`); + } + */ +} + +Toolbar.propTypes = { + application: React.PropTypes.object.isRequired, + editor: React.PropTypes.object.isRequired, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx new file mode 100644 index 0000000000..529ae92ded --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx @@ -0,0 +1,31 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import React from 'react'; + +const Export = function Export() { + return ( +
+ + + + + +
+ ); +}; + +export default Export; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx new file mode 100644 index 0000000000..6bc04f997f --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx @@ -0,0 +1,41 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import React from 'react'; + +/** + * Simple icon view. + * @param glyph glyph definition, from import. + * @param className optional classname, for svg element. + * @returns {XML} + * @constructor + */ +const Icon = function Icon({ glyph, className }) { + return ( + + + + ); +}; + +/** Declare properties. */ +Icon.propTypes = { + className: React.PropTypes.string, + glyph: React.PropTypes.string.isRequired, +}; + +export default Icon; + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx new file mode 100644 index 0000000000..817f4f1697 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx @@ -0,0 +1,61 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import React from 'react'; + +/** + * Overlay view. + */ +export default class Overlay extends React.Component { + + /** + * Construct view. + * @param props element properties. + * @param context react context. + */ + constructor(props, context) { + super(props, context); + this.state = { + visible: false, + }; + this.setVisible = this.setVisible.bind(this); + } + + /** + * Set visibility. + * @param visible true if visible. + */ + setVisible(visible) { + this.setState({ + visible, + }); + } + + /** + * Render view. + * @returns {XML} + */ + render() { + const display = this.state.visible ? 'block' : 'none'; + return ( +
+
+ ); + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodel.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodel.js new file mode 100644 index 0000000000..82e8ada588 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodel.js @@ -0,0 +1,94 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import _merge from 'lodash/merge'; + +import Common from '../common/Common'; + +/** + * Rules governing what a definition can contain. + */ +export default class Metamodel { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct from JSON definition. + * @param json schema definition. + */ + constructor(json) { + Common.assertType(json, 'Object'); + const dfault = require('./templates/default.metamodel.json'); + this.json = _merge({}, dfault, json); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get schema identifier. + * @returns ID. + */ + getId() { + return this.json.diagram.metadata.id; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get lifeline constraints. + * @returns {*} + */ + getConstraints() { + return this.json.diagram.lifelines.constraints; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get lifeline metadata by lifeline ID. + * @param id sought lifeline. + * @returns lifeline if found. + */ + getLifelineById(id) { + for (const lifeline of this.json.diagram.lifelines.lifelines) { + if (lifeline.id === id) { + return lifeline; + } + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get original JSON. + * @returns JSON. + */ + unwrap() { + return this.json; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get default schema. + * @returns Metamodel default (permissive) Metamodel. + */ + static getDefault() { + return new Metamodel({}); + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodels.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodels.js new file mode 100644 index 0000000000..4ecfc0b5f7 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodels.js @@ -0,0 +1,87 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import Common from '../common/Common'; +import Metamodel from './Metamodel'; + +/** + * A simple lookup for schemas by ID. + */ +export default class Metamodels { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct metamodels from provided JSON definitions. + * @param metamodels JSON metamodel definitions. + */ + constructor(metamodels) { + + Common.assertType(metamodels, 'Array'); + + this.lookup = {}; + + // Save each metamodel. It's up to the Metamodel class to make sense of + // potentially nonsense metamodel definitions. + + for (const json of metamodels) { + const metamodel = new Metamodel(json); + this.lookup[metamodel.getId()] = metamodel; + } + + // Set (or override) the default metamodel with the inlined one. + + this.lookup.$ = Metamodel.getDefault(); + Common.assertInstanceOf(this.lookup.$, Metamodel); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get Metamodel by its @id. + * @param id identifier. + * @returns Metamodel, or undefined if no matching metamodel found. + */ + getMetamodel(id) { + return this.lookup[id]; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get the default (permissive) metamodel. + * @returns default Metamodel. + */ + getDefault() { + return this.lookup.$; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get metamodel by its @id, falling back to the default. + * @param id identifier. + * @returns matching metamodel, or default. + */ + getMetamodelOrDefault(id) { + const metamodel = this.getMetamodel(id); + if (metamodel) { + return metamodel; + } + return this.getDefault(); + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Model.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Model.js new file mode 100644 index 0000000000..1e68cd6034 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Model.js @@ -0,0 +1,512 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +import _merge from 'lodash/merge'; +// import jsonschema from 'jsonschema'; + +import Common from '../common/Common'; +import Metamodel from './Metamodel'; + +/** + * A wrapper for a model instance. + */ +export default class Model { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct model from model JSON. JSON is assumed to be in more or less + * the correct structure, but it's OK if it's missing IDs. + * + * @param json initial JSON; will be updated in situ. + * @param metamodel Metaobject definition. + */ + constructor(json, metamodel) { + + if (metamodel) { + Common.assertInstanceOf(metamodel, Metamodel); + } + + this.metamodel = metamodel || Metamodel.getDefault(); + Common.assertInstanceOf(this.metamodel, Metamodel); + + this.jsonschema = require('./schema/asdc_sequencer_schema.json'); + this.templates = { + defaultModel: require('./templates/default.model.json'), + defaultMetamodel: require('./templates/default.metamodel.json'), + }; + + this.model = this._preprocess(Common.assertType(json, 'Object')); + Common.assertPlainObject(this.model); + + this.renumber(); + + this.addLifeline = this.addLifeline.bind(this); + this.addMessage = this.addMessage.bind(this); + this.renumber = this.renumber.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Unwrap to get model object. + * @returns {*} + */ + unwrap() { + return Common.assertPlainObject(this.model); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get the metamodel which defines valid states for this model. + * @returns Metamodel definition. + */ + getMetamodel() { + return Common.assertInstanceOf(this.metamodel, Metamodel); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Find lifeline by its ID. + * @param id lifeline ID. + * @returns lifeline object, if found. + */ + getLifelineById(id) { + for (const lifeline of this.model.diagram.lifelines) { + if (lifeline.id === id) { + return lifeline; + } + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get message by ID. + * @param id message ID. + * @returns message if matched. + */ + getMessageById(id) { + Common.assertNotNull(id); + const step = this.getStepByMessageId(id); + if (step) { + return step.message; + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get step by message ID. + * @param id step ID. + * @returns step if matched. + */ + getStepByMessageId(id) { + Common.assertNotNull(id); + for (const step of this.model.diagram.steps) { + if (step.message && step.message.id === id) { + return step; + } + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add message to steps. + * @returns {{}} + */ + addMessage(index) { + const d = this.model.diagram; + const step = {}; + step.message = {}; + step.message.id = Model._guid(); + step.message.name = '[Unnamed Message]'; + step.message.type = 'request'; + step.message.from = d.lifelines.length > 0 ? d.lifelines[0].id : -1; + step.message.to = d.lifelines.length > 1 ? d.lifelines[1].id : -1; + if (index >= 0) { + d.steps.splice(index, 0, step); + } else { + d.steps.push(step); + } + this.renumber(); + return step; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete message with ID. + * @param id to be deleted. + */ + deleteMessageById(id) { + Common.assertNotNull(id); + const step = this.getStepByMessageId(id); + if (step) { + const index = this.model.diagram.steps.indexOf(step); + if (index !== -1) { + this.model.diagram.steps.splice(index, 1); + } + } + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add lifeline to lifelines. + * @param index optional index. + * @returns {{}} + */ + addLifeline(index) { + const lifeline = {}; + lifeline.id = Model._guid(); + lifeline.name = '[Unnamed Lifeline]'; + if (index >= 0) { + this.model.diagram.lifelines.splice(index, 0, lifeline); + } else { + this.model.diagram.lifelines.push(lifeline); + } + this.renumber(); + return lifeline; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete lifeline with ID. + * @param id to be deleted. + */ + deleteLifelineById(id) { + Common.assertNotNull(id); + this.deleteStepsByLifelineId(id); + const lifeline = this.getLifelineById(id); + if (lifeline) { + const index = this.model.diagram.lifelines.indexOf(lifeline); + if (index !== -1) { + this.model.diagram.lifelines.splice(index, 1); + } + } + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete all steps corresponding to lifeline. + * @param id lifeline ID. + */ + deleteStepsByLifelineId(id) { + Common.assertNotNull(id); + const steps = this.getStepsByLifelineId(id); + for (const step of steps) { + this.deleteMessageById(step.message.id); + } + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get all steps corresponding to lifeline. + * @param id lifeline ID. + * @return steps from/to lifeline. + */ + getStepsByLifelineId(id) { + Common.assertNotNull(id); + const steps = []; + for (const step of this.model.diagram.steps) { + if (step.message) { + if (step.message.from === id || step.message.to === id) { + steps.push(step); + } + } + } + return steps; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Validate model. Disabled, because we removed the jsonschema dependency. + * @returns {Array} of validation errors, if any. + */ + validate() { + const errors = []; + return errors; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Reorder messages. + * @param index message index. + * @param afterIndex new (after) index. + */ + reorderMessages(index, afterIndex) { + Common.assertType(index, 'Number'); + Common.assertType(afterIndex, 'Number'); + const steps = this.model.diagram.steps; + const element = steps[index]; + steps.splice(index, 1); + steps.splice(afterIndex, 0, element); + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Reorder lifelines. + * @param index lifeline index. + * @param afterIndex new (after) index. + */ + reorderLifelines(index, afterIndex) { + Common.assertType(index, 'Number'); + Common.assertType(afterIndex, 'Number'); + const lifelines = this.model.diagram.lifelines; + const element = lifelines[index]; + lifelines.splice(index, 1); + lifelines.splice(afterIndex, 0, element); + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Renumber lifelines and messages. + */ + renumber() { + const modelJSON = this.unwrap(); + let stepIndex = 1; + let lifelineIndex = 1; + for (const step of modelJSON.diagram.steps) { + if (step.message) { + step.message.index = stepIndex++; + } + } + for (const lifeline of modelJSON.diagram.lifelines) { + lifeline.index = lifelineIndex++; + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Build a simple, navigable dataset describing fragments. + * @returns {{}}, indexed by (stop) message ID, describing fragments. + */ + analyzeFragments() { + + const fData = {}; + + let depth = 0; + const modelJSON = this.unwrap(); + const open = []; + + const getData = function g(stop, fragment) { + let data = fData[stop]; + if (!data) { + data = { stop, start: [], fragment }; + fData[stop] = data; + } + return data; + }; + + const fragmentsByStart = {}; + for (const step of modelJSON.diagram.steps) { + if (step.message && step.message.fragment) { + const message = step.message; + const fragment = message.fragment; + if (fragment.start) { + fragmentsByStart[fragment.start] = fragment; + open.push(message.id); + depth++; + } + if (fragment.stop) { + if (open.length > 0) { + getData(message.id).start.push(open.pop()); + } + depth = Math.max(depth - 1, 0); + } + } + } + + if (open.length > 0) { + for (const o of open) { + getData(o, fragmentsByStart[o]).start.push(o); + } + } + + return fData; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Build a simple, navigable dataset describing occurrences. + * @returns a map, indexed by lifeline ID, of objects containing {start:[],stop:[],active[]}. + * @private + */ + analyzeOccurrences() { + + const oData = {}; + + // A few inline functions. They make this method kinda lengthy, but they + // reduce clutter in the class and keep it coherent, so it's OK. + + const getDataByLifelineId = function get(lifelineId) { + if (!oData[lifelineId]) { + oData[lifelineId] = { active: [], start: {}, stop: {} }; + } + return oData[lifelineId]; + }; + + const contains = function contains(array, value) { + return (array && (array.indexOf(value) !== -1)); + }; + + const process = function process(message, lifelineId) { + const oRule = message.occurrences; + if (oRule) { + + const oDataLifeline = getDataByLifelineId(lifelineId); + if (oDataLifeline) { + + // Record all starts. + + if (contains(oRule.start, lifelineId)) { + oDataLifeline.active.push(message.id); + oDataLifeline.start[message.id] = undefined; + } + + // Reconcile with stops. + + if (contains(oRule.stop, lifelineId)) { + const startMessageId = oDataLifeline.active.pop(); + oDataLifeline.stop[message.id] = startMessageId; + if (startMessageId) { + oDataLifeline.start[startMessageId] = message.id; + } + } + } + } + }; + + // Analyze start and end. + + const modelJSON = this.unwrap(); + for (const step of modelJSON.diagram.steps) { + if (step.message) { + const message = step.message; + if (message.occurrences) { + process(message, message.from); + process(message, message.to); + } + } + } + + // Reset active. (We used it, but it's not actually for us; it's for keeping + // track of active occurrences when rendering the diagram.) + + for (const lifelineId of Object.keys(oData)) { + oData[lifelineId].active = []; + } + + // Reconcile the start and end (message ID) maps for each lifeline, + // finding a "stop" for every start. Default to starting and stopping + // on the same message, which is the same as no occurrence. + + for (const lifelineId of Object.keys(oData)) { + const lifelineData = oData[lifelineId]; + for (const startId of Object.keys(lifelineData.start)) { + const stopId = lifelineData.start[startId]; + if (!stopId) { + lifelineData.start[startId] = startId; + lifelineData.stop[startId] = startId; + } + } + } + + return oData; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Preprocess model, adding IDs and whatnot. + * @param original to be preprocessed. + * @returns preprocessed JSON. + * @private + */ + _preprocess(original) { + + const json = _merge({}, this.templates.defaultModel, original); + const metamodel = this.metamodel.unwrap(); + if (!json.diagram.metadata.ref) { + if (metamodel.diagram.metadata.id) { + json.diagram.metadata.ref = metamodel.diagram.metadata.id; + } else { + json.diagram.metadata.ref = '$'; + } + } + + for (const lifeline of json.diagram.lifelines) { + lifeline.id = lifeline.id || lifeline.name; + } + + for (const step of json.diagram.steps) { + if (step.message) { + step.message.id = step.message.id || Model._guid(); + const occurrences = step.message.occurrences; + if (occurrences) { + occurrences.start = occurrences.start || []; + occurrences.stop = occurrences.stop || []; + } + } + } + + if (!json.diagram.metadata.id || json.diagram.metadata.id === '$') { + json.diagram.metadata.id = Model._guid(); + } + + return json; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Create pseudo-UUID. + * @returns {string} + * @private + */ + static _guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return `${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}`; + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/Scenarios.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/Scenarios.js new file mode 100644 index 0000000000..4130ec7ec3 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/Scenarios.js @@ -0,0 +1,110 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * 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. + */ + +/** + * Example scenarios, for development, testing and demos. + */ +export default class Scenarios { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct scenarios; read model and metamodel templates. + */ + constructor() { + this.templates = { + model: { + ecomp: require('./model/ECOMP.json'), + blank: require('./model/BLANK.json'), + dimensions: require('./model/DIMENSIONS.json'), + }, + metamodel: { + ecomp: require('./metamodel/ECOMP.json'), + blank: require('./metamodel/BLANK.json'), + }, + }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get ECOMP scenario. + * @return ECOMP scenario JSON. + */ + getECOMP() { + return JSON.parse(JSON.stringify(this.templates.model.ecomp)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get ECOMP scenario metamodel. + * @return scenario metamodel JSON. + */ + getECOMPMetamodel() { + return JSON.parse(JSON.stringify(this.templates.metamodel.ecomp)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get blank scenario. + * @return blank scenario JSON. + */ + getBlank() { + return JSON.parse(JSON.stringify(this.templates.model.blank)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get empty scenario metamodel. + * @return empty metamodel JSON. + */ + getBlankMetamodel() { + return JSON.parse(JSON.stringify(this.templates.metamodel.blank)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get scenario. + * @return scenario JSON. + */ + getDimensions() { + return JSON.parse(JSON.stringify(this.templates.model.dimensions)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get scenario metamodel. + * @return metamodel JSON. + */ + getDimensionsMetamodel() { + return JSON.parse(JSON.stringify(this.templates.metamodel.blank)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get demo metamodels. + * @returns {*[]} + */ + getMetamodels() { + return [this.getBlankMetamodel(), this.getDimensionsMetamodel(), this.getECOMPMetamodel()]; + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/BLANK.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/BLANK.json new file mode 100644 index 0000000000..2853405883 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/BLANK.json @@ -0,0 +1,16 @@ +{ + "diagram": { + "metadata": { + "id": "BLANK", + "name": "Blank" + }, + "lifelines": { + "lifelines": [], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/DIMENSIONS.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/DIMENSIONS.json new file mode 100644 index 0000000000..f02111d0f3 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/DIMENSIONS.json @@ -0,0 +1,16 @@ +{ + "diagram": { + "metadata": { + "id": "DIMENSIONS", + "name": "Dimensions" + }, + "lifelines": { + "lifelines": [], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/ECOMP.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/ECOMP.json new file mode 100644 index 0000000000..939c1398b5 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/ECOMP.json @@ -0,0 +1,62 @@ +{ + "diagram": { + "metadata": { + "id": "ECOMP", + "name": "ECOMP" + + }, + "lifelines": { + "lifelines": [{ + "id": "1", + "name": "Customer" + }, { + "id": "2", + "name": "MSO" + }, { + "id": "3", + "name": "SDN" + }, { + "id": "4", + "name": "A&AI" + }, { + "id": "5", + "name": "IPE TOR" + }, { + "id": "6", + "name": "ORM" + }, { + "id": "7", + "name": "ORD" + }, { + "id": "8", + "name": "Heat" + }, { + "id": "9", + "name": "NovaAPI" + }, { + "id": "10", + "name": "Ntrn Contrl" + }, { + "id": "11", + "name": "RO" + }, { + "id": "12", + "name": "Nova Agent" + }, { + "id": "13", + "name": "VF Agent" + }, { + "id": "14", + "name": "Hypervisor" + }, { + "id": "15", + "name": "VF" + }], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/BLANK.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/BLANK.json new file mode 100644 index 0000000000..784a80e820 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/BLANK.json @@ -0,0 +1,37 @@ +{ + "diagram": { + "metadata": { + "id": "$", + "ref": "BLANK", + "name": "New Sequence" + }, + "lifelines": [ + { + "id": "Alice", + "name": "Alice" + }, + { + "id": "Bob", + "name": "Bob" + } + ], + "steps": [ + { + "message": { + "from": "Alice", + "to": "Bob", + "label": "Sup Bob", + "type": "request" + } + }, + { + "message": { + "from": "Bob", + "to": "Alice", + "label": "Yo Alice", + "type": "response" + } + } + ] + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/DIMENSIONS.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/DIMENSIONS.json new file mode 100644 index 0000000000..642e34a785 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/DIMENSIONS.json @@ -0,0 +1,91 @@ +{ + "diagram": { + "metadata": { + "id": "DIMENSIONS1", + "name": "Dimensions Test", + "ref": "DIMENSIONS" + }, + "lifelines": [ + { + "id": "L01", + "name": "Lorum Ipsum" + }, + { + "id": "L02", + "name": "Donec nisi urna, porttitor efficitur felis vel, efficitur consequat nunc" + }, + { + "id": "L03", + "name": "Mauris dignissim SphymomanometerSphymomanometer enim non sapien tristique lacinia" + } + ], + "steps": [ + { + "message": { + "id": "M01", + "from": "L01", + "to": "L02", + "name": "Morbi", + "type": "request", + "notes": [ + "Proin non libero malesuada." + ], + "fragment": { + "operator": "alt", + "start": true, + "guard": "Curabitur sollicitudin nulla elit, et ultrices tortor faucibus quis" + }, + "occurrences": { + "start": ["L01", "L02"], + "stop": [] + } + } + }, + { + "message": { + "id": "M02", + "from": "L02", + "to": "L03", + "name": "Quisque pretium tellus sit amet congue dictum. Mauris ac rutrum arcu, et fringilla orci", + "type": "request", + "notes": [ + "Nam quis felis hendrerit, lacinia ipsum vitae, faucibus elit. Morbi sit amet nunc eget massa vehicula rhoncus sit amet vel tellus. Aliquam accumsan eros elit, et sollicitudin lacus vehicula eu. Aenean rhoncus justo ut felis tincidunt, sit amet vulputate metus aliquet. Phasellus tellus est, consequat nec ex mollis, lacinia vestibulum justo. Nam quis felis hendrerit, lacinia ipsum vitae, faucibus elit. Morbi sit amet nunc eget massa vehicula rhoncus sit amet vel tellus. Aliquam accumsan eros elit, et sollicitudin lacus vehicula eu. Aenean rhoncus justo ut felis tincidunt, sit amet vulputate metus aliquet. Phasellus tellus est, consequat nec ex mollis, lacinia vestibulum justo." + ], + "occurrences": { + "start": [], + "stop": ["L02"] + } + } + }, + { + "message": { + "id": "M03", + "from": "L01", + "to": "L03", + "name": "Nullam", + "type": "response", + "fragment": { + "stop": true + }, + "occurrences": { + "start": [], + "stop": ["L01"] + } + } + }, + { + "message": { + "id": "M04", + "from": "L01", + "to": "L03", + "name": "Etiam convallis augue est. ", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + } + ] + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/ECOMP.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/ECOMP.json new file mode 100644 index 0000000000..dd9bfc5eb0 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/ECOMP.json @@ -0,0 +1,514 @@ +{ + "diagram": { + "metadata": { + "id": "ECOMP1", + "name": "Detailed flow in Ecomp1", + "ref": "ECOMP" + }, + "lifelines": [ + { + "id": "L01", + "name": "Customer" + }, + { + "id": "L02", + "name": "MSO" + }, + { + "id": "L03", + "name": "SDN" + }, + { + "id": "L04", + "name": "A&AI" + }, + { + "id": "L05", + "name": "IPE TOR" + }, + { + "id": "L06", + "name": "ORM" + }, + { + "id": "L07", + "name": "ORD" + }, + { + "id": "L08", + "name": "Heat" + }, + { + "id": "L09", + "name": "NovaAPI" + }, + { + "id": "L10", + "name": "Ntrn Contrl" + }, + { + "id": "L11", + "name": "RO" + }, + { + "id": "L12", + "name": "Nova Agent" + }, + { + "id": "L13", + "name": "VF Agent" + }, + { + "id": "L14", + "name": "Hypervisor" + }, + { + "id": "L15", + "name": "VF" + } + ], + "steps": [ + { + "message": { + "id": "M01", + "from": "L01", + "to": "L02", + "name": "Create", + "type": "request", + "notes": [ + "This note is short." + ], + "occurrences": { + "start": ["L01", "L02"], + "stop": [] + } + } + }, + { + "message": { + "id": "M02", + "from": "L02", + "to": "L04", + "name": "Check Tenant", + "type": "request", + "occurrences": { + "start": ["L02"], + "stop": [] + } + } + }, + { + "message": { + "id": "M03", + "from": "L02", + "to": "L06", + "name": "Create Tenant", + "type": "request", + "fragment": { + "operator": "alt", + "start": true, + "guard": "Does not exist" + }, + "occurrences": { + "start": ["L06"], + "stop": [] + } + } + }, + { + "message": { + "id": "M04", + "from": "L06", + "to": "L07", + "name": "Distribute", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M05", + "from": "L06", + "to": "L02", + "name": "Async Response", + "type": "response", + "asynchronous": true, + "fragment": { + "stop": true + }, + "occurrences": { + "start": [], + "stop": ["L02", "L06"] + } + } + }, + { + "message": { + "id": "M06", + "from": "L07", + "to": "L08", + "name": "Push", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M07", + "from": "L08", + "to": "L02", + "name": "Tenant Complete", + "type": "response", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M08", + "from": "L02", + "to": "L03", + "name": "Service Topology", + "type": "request", + "occurrences": { + "start": ["L03"], + "stop": [] + } + } + }, + { + "message": { + "id": "M09", + "from": "L03", + "to": "L05", + "name": "Pre-configs", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M10", + "from": "L03", + "to": "L04", + "name": "Retrieve and populate", + "type": "request", + "occurrences": { + "start": [], + "stop": ["L03"] + } + } + }, + { + "message": { + "id": "M11", + "from": "L02", + "to": "L08", + "name": "VNF PreRequisite Heat Template", + "type": "request", + "notes": [ + "I got up and made coffee and read my emails and answered them until I got frustrated and made a mental note to answer the others later and then looked out of the window for a while and then made more coffee." + ], + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M12", + "from": "L08", + "to": "L10", + "name": "Provider and OAM nw", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M13", + "from": "L02", + "to": "L08", + "name": "Get Stack Status", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M14", + "from": "L08", + "to": "L02", + "name": "Status complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M15", + "from": "L11", + "to": "L04", + "name": "Provider and OAM Inventory", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M16", + "from": "L02", + "to": "L08", + "name": "VNF Server Heat Template", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M17", + "from": "L08", + "to": "L10", + "name": "Show Port", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M18", + "from": "L11", + "to": "L02", + "name": "Async Response with Stack ID", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M19", + "from": "L10", + "to": "L08", + "name": "Response", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M20", + "from": "L08", + "to": "L09", + "name": "Nova VM", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M21", + "from": "L09", + "to": "L12", + "name": "Scheduler Picks Nova Agent", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M22", + "from": "L12", + "to": "L14", + "name": "Picks VF", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M23", + "from": "L12", + "to": "L10", + "name": "Retrieves Port Info", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M24", + "from": "L12", + "to": "L13", + "name": "Calls CF Agent", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M25", + "from": "L13", + "to": "L15", + "name": "Configure VF", + "type": "response", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M26", + "from": "L15", + "to": "L13", + "name": "Response", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M27", + "from": "L13", + "to": "L12", + "name": "Complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M28", + "from": "L12", + "to": "L08", + "name": "Response Complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M29", + "from": "L11", + "to": "L04", + "name": "VServer and Show Port Inventory", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M30", + "from": "L02", + "to": "L08", + "name": "Get Stack Status", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M31", + "from": "L08", + "to": "L02", + "name": "Stack Status Complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M32", + "from": "L02", + "to": "L01", + "name": "Done", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": ["L01", "L02"] + } + } + } + ] + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-meta-schema.xsd b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-meta-schema.xsd new file mode 100644 index 0000000000..f75063bed5 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-meta-schema.xsd @@ -0,0 +1,166 @@ + + + + + + + + + Diagram meta-schema, defining what diagram documents may look like. + + The main difference between the metaschema (this) and the schema, is that + the metaschema describes what's *allowed* rather than what *is*. + + Specific differences: + + 1. The metaschema exists primarily to constrain lifelines; to declare any + that are predefined, to prescribe cardinality, order and whether or not + ad hoc lifelines may be created by the user. + 2. The metaschema doesn't constrain messages at all. This may come along later, + but for now they're freetext, and can be defined between any legal pair + of lifelines. + 3. The metaschema doesn't have @ref attributes; its @id attributes are the + target of @ref attributes in the instance schema.m + + + + + + + + + + + + + + + + + Common attributes, most importantly @id, which every entity must have. + + + + + + + + + + + + + + + Schema definition identifier. + + + + + + + Human-readable name. + + + + + + + + + + + Diagram metadata, including: + - Unique ID, referenced by @ref attributes in instance documents. + - Human-readable description, displayed on-screen. + + + + + + + + + + + + + Metadata concerning a single lifeline. + + + + + + + + Whether an instance may omit this lifeline. Only takes effect + where the lifelines setting is @delete=true. + + + + + + + + + + + + + Metadata concerning allowed lifelines. Somewhat more strict that + instance data. + + + + + + + + + + + + Whether the user may create their own lifelines. + + + + + + + Whether declared lifelines may be deleted. + See also @mandatory on lifeline. + + + + + + + Whether lifelines may be reordered. + + + + + + + + + + + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-schema.xsd b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-schema.xsd new file mode 100644 index 0000000000..71a7d07cb1 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-schema.xsd @@ -0,0 +1,274 @@ + + + + + + + + + + Diagram state. + + + + + + + + + + + + + + + + + Stuff common to all entities; an identifier, a name, an optional + schema reference, and some optional notes. + + + + + + + + + + + + + + Optional annotations; non-structural information attached to any entity. + + + + + + + + + + + + + Entity identifier. + + + + + + + Optional reference to schema definition, where this entity + corresponds to (and is constrained by) a schema entity. + + + + + + + Human-readable name. + + + + + + + ID of entity in originating system. For external use; not + used by the sequencer widget. + + + + + + + + + + + + Diagram metadata, including name, identifier and schema reference. + + + + + + + + + + + + + Definition of a single lifeline. + + + + + + + + + + + + + A set of lifelines. May be top-level or in a fragment. + + + + + + + + + + + + + + + + + An occurrence at one or other end of a message. + + + + + + + + + + + + + + + + + + + + A fragment directive. + + + + + + Whether fragment starts; fragment activated when @start=true. + + + + + + + Indication of the last message in this fragment. + + + + + + + Fragment operation. Start with the three everybody knows, but + there are others. + + + + + + + + + + + + + + Guard condition. + + + + + + + + + + + A message between lifelines. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Guard condition within a fragment. Some fragments have more than + one section, each with their own guard condition. + + + + + + + + + + + + + + An ordered set of messages and subsequences. + + + + + + + + + + + + + + diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_meta_schema.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_meta_schema.json new file mode 100644 index 0000000000..cf4174ed35 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_meta_schema.json @@ -0,0 +1,332 @@ + +{ + "id":"#", + "definitions":{ + "LifelinesType.Constraints":{ + "type":"object", + "title":"LifelinesType.Constraints", + "required":[ + "create", + "delete", + "reorder" + ], + "properties":{ + "create":{ + "title":"create", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"create", + "namespaceURI":"" + } + }, + "delete":{ + "title":"delete", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"delete", + "namespaceURI":"" + } + }, + "reorder":{ + "title":"reorder", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"reorder", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "create", + "delete", + "reorder" + ] + }, + "LifelinesType":{ + "required":[ + "constraints" + ], + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelinesType", + "properties":{ + "lifeline":{ + "title":"lifeline", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"#/definitions/LifelineType" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifeline", + "namespaceURI":"" + } + }, + "constraints":{ + "title":"constraints", + "allOf":[ + { + "$ref":"#/definitions/LifelinesType.Constraints" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"constraints", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelinesType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "lifeline", + "constraints" + ] + }, + "EntityType.Notes":{ + "type":"object", + "title":"EntityType.Notes", + "properties":{ + "note":{ + "title":"note", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"note", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "note" + ] + }, + "MetadataType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"MetadataType", + "properties":{ + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"metadataType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + }, + "EntityType":{ + "type":"object", + "title":"EntityType", + "required":[ + "id", + "name" + ], + "properties":{ + "notes":{ + "title":"notes", + "allOf":[ + { + "$ref":"#/definitions/EntityType.Notes" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"notes", + "namespaceURI":"" + } + }, + "id":{ + "title":"id", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"id", + "namespaceURI":"" + } + }, + "name":{ + "title":"name", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"name", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"entityType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "notes", + "id", + "name" + ] + }, + "Diagram":{ + "type":"object", + "title":"Diagram", + "required":[ + "metadata", + "lifelines" + ], + "properties":{ + "metadata":{ + "title":"metadata", + "allOf":[ + { + "$ref":"#/definitions/MetadataType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"metadata", + "namespaceURI":"" + } + }, + "lifelines":{ + "title":"lifelines", + "allOf":[ + { + "$ref":"#/definitions/LifelinesType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifelines", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "metadata", + "lifelines" + ] + }, + "LifelineType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelineType", + "properties":{ + "mandatory":{ + "title":"mandatory", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"mandatory", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelineType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "mandatory" + ] + } + }, + "anyOf":[ + { + "type":"object", + "properties":{ + "name":{ + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/QName" + }, + { + "type":"object", + "properties":{ + "localPart":{ + "enum":[ + "diagram" + ] + }, + "namespaceURI":{ + "enum":[ + "http://ns.ecomp.com/asdc/sequencer" + ] + } + } + } + ] + }, + "value":{ + "$ref":"#/definitions/Diagram" + } + }, + "elementName":{ + "localPart":"diagram", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + } + ] +} \ No newline at end of file diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_schema.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_schema.json new file mode 100644 index 0000000000..d655826290 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_schema.json @@ -0,0 +1,582 @@ + +{ + "id":"#", + "definitions":{ + "EntityType.Notes":{ + "type":"object", + "title":"EntityType.Notes", + "properties":{ + "note":{ + "title":"note", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"note", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "note" + ] + }, + "GuardType":{ + "type":"object", + "title":"GuardType", + "required":[ + "guard", + "steps" + ], + "properties":{ + "guard":{ + "title":"guard", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"guard", + "namespaceURI":"" + } + }, + "steps":{ + "title":"steps", + "allOf":[ + { + "$ref":"#/definitions/StepsType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"steps", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"guardType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "guard", + "steps" + ] + }, + "MetadataType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"MetadataType", + "properties":{ + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"metadataType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + }, + "OccurrencesType":{ + "type":"object", + "title":"OccurrencesType", + "properties":{ + "start":{ + "title":"start", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"start", + "namespaceURI":"" + } + }, + "stop":{ + "title":"stop", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"stop", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"occurrencesType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "start", + "stop" + ] + }, + "Diagram":{ + "type":"object", + "title":"Diagram", + "required":[ + "metadata", + "lifelines", + "steps" + ], + "properties":{ + "metadata":{ + "title":"metadata", + "allOf":[ + { + "$ref":"#/definitions/MetadataType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"metadata", + "namespaceURI":"" + } + }, + "lifelines":{ + "title":"lifelines", + "allOf":[ + { + "$ref":"#/definitions/LifelinesType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifelines", + "namespaceURI":"" + } + }, + "steps":{ + "title":"steps", + "allOf":[ + { + "$ref":"#/definitions/StepsType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"steps", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "metadata", + "lifelines", + "steps" + ] + }, + "LifelineType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelineType", + "properties":{ + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelineType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + }, + "LifelinesType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelinesType", + "properties":{ + "lifeline":{ + "title":"lifeline", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"#/definitions/LifelineType" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifeline", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelinesType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "lifeline" + ] + }, + "FragmentType":{ + "type":"object", + "title":"FragmentType", + "properties":{ + "start":{ + "title":"start", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"start", + "namespaceURI":"" + } + }, + "stop":{ + "title":"stop", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"stop", + "namespaceURI":"" + } + }, + "operation":{ + "title":"operation", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"operation", + "namespaceURI":"" + } + }, + "guard":{ + "title":"guard", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"guard", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"fragmentType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "start", + "stop", + "operation", + "guard" + ] + }, + "StepsType":{ + "required":[ + "message" + ], + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"StepsType", + "properties":{ + "message":{ + "title":"message", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"#/definitions/MessageType" + }, + "minItems":1 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"message", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"stepsType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "message" + ] + }, + "EntityType":{ + "type":"object", + "title":"EntityType", + "required":[ + "id", + "name" + ], + "properties":{ + "notes":{ + "title":"notes", + "allOf":[ + { + "$ref":"#/definitions/EntityType.Notes" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"notes", + "namespaceURI":"" + } + }, + "id":{ + "title":"id", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"id", + "namespaceURI":"" + } + }, + "ref":{ + "title":"ref", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"ref", + "namespaceURI":"" + } + }, + "name":{ + "title":"name", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"name", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"entityType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "notes", + "id", + "ref", + "name" + ] + }, + "MessageType":{ + "required":[ + "to", + "from" + ], + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"MessageType", + "properties":{ + "occurrences":{ + "title":"occurrences", + "allOf":[ + { + "$ref":"#/definitions/OccurrencesType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"occurrences", + "namespaceURI":"" + } + }, + "fragment":{ + "title":"fragment", + "allOf":[ + { + "$ref":"#/definitions/FragmentType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"fragment", + "namespaceURI":"" + } + }, + "to":{ + "title":"to", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"to", + "namespaceURI":"" + } + }, + "from":{ + "title":"from", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"from", + "namespaceURI":"" + } + }, + "type":{ + "title":"type", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"type", + "namespaceURI":"" + } + }, + "asynchronous":{ + "title":"asynchronous", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"asynchronous", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"messageType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "occurrences", + "fragment", + "to", + "from", + "type", + "asynchronous" + ] + } + }, + "anyOf":[ + { + "type":"object", + "properties":{ + "name":{ + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/QName" + }, + { + "type":"object", + "properties":{ + "localPart":{ + "enum":[ + "diagram" + ] + }, + "namespaceURI":{ + "enum":[ + "http://ns.ecomp.com/asdc/sequencer" + ] + } + } + } + ] + }, + "value":{ + "$ref":"#/definitions/Diagram" + } + }, + "elementName":{ + "localPart":"diagram", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + } + ] +} \ No newline at end of file diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.metamodel.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.metamodel.json new file mode 100644 index 0000000000..f6a28a8723 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.metamodel.json @@ -0,0 +1,17 @@ +{ + "diagram": { + "metadata": { + "id": "$", + "name": "Blank Sequence" + + }, + "lifelines": { + "lifelines": [], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.model.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.model.json new file mode 100644 index 0000000000..42edc5516b --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.model.json @@ -0,0 +1,11 @@ +{ + "diagram": { + "metadata": { + "id": "$", + "name": "New Sequence" + + }, + "lifelines": [], + "steps": [] + } +} -- cgit 1.2.3-korg