diff --git a/samples/spring-flo-si/LICENSE.txt b/samples/spring-flo-si/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/samples/spring-flo-si/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/samples/spring-flo-si/README.adoc b/samples/spring-flo-si/README.adoc
new file mode 100644
index 0000000..f02deac
--- /dev/null
+++ b/samples/spring-flo-si/README.adoc
@@ -0,0 +1,42 @@
+# A sample app using spring-flo to visualize Spring Integration applications
+
+This sample uses flo as a live viewer for Spring Integration applications.
+
+This https://spring.io/blog/2016/04/26/spring-integration-4-3-m2-is-available[blog] discusses
+how to activate the new endpoint in a Spring Integration application. When
+the endpoint is active in your SI application, just enter that into the spring
+flo viewer. You should then see something like this:
+
+image::imgs/basicGraph.png[width="800"]
+
+# Running the sample
+
+A basic Spring Boot app is used to serve the sample. Launch it with:
+
+ mvn spring-boot:run
+
+then open `http://localhost:8082`. In the `Spring Integration Graph Endpoint`
+field enter the url for the spring integration data, for example: `http://localhost:8080/integration`
+and the graph should load.
+
+
+# Using the application
+
+Once the graph is loaded you can drag nodes around to adjust the layout. (Press the `Read-Only`
+button to prevent moving nodes around). Hovering over a node will show a tool tip with more
+information for that element. If you hover over a channel you will see many stats about
+traffic flowing over that channel:
+
+image::imgs/tooltip.png[width="500"]
+
+It is possible to select one of those stats of interest and have it shown directly on the graph.
+Simply select what you are interested in and enter the name of that stat in the `Link label path`
+field at the top. The values for that stat will then be shown on the links between graph
+elements:
+
+image::imgs/numbersGraph.png[width="800"]
+
+If you enter a Refresh rate (minimal allowed is 250ms) then that stat will actually
+update on the graph at that rate with a small animation indicating where on the graph changes
+in value are occurring.
+
diff --git a/samples/spring-flo-si/imgs/basicGraph.png b/samples/spring-flo-si/imgs/basicGraph.png
new file mode 100644
index 0000000..66104fe
Binary files /dev/null and b/samples/spring-flo-si/imgs/basicGraph.png differ
diff --git a/samples/spring-flo-si/imgs/numbersGraph.png b/samples/spring-flo-si/imgs/numbersGraph.png
new file mode 100644
index 0000000..e9a0fac
Binary files /dev/null and b/samples/spring-flo-si/imgs/numbersGraph.png differ
diff --git a/samples/spring-flo-si/imgs/tooltip.png b/samples/spring-flo-si/imgs/tooltip.png
new file mode 100644
index 0000000..f0ee064
Binary files /dev/null and b/samples/spring-flo-si/imgs/tooltip.png differ
diff --git a/samples/spring-flo-si/pom.xml b/samples/spring-flo-si/pom.xml
new file mode 100755
index 0000000..cfaecbb
--- /dev/null
+++ b/samples/spring-flo-si/pom.xml
@@ -0,0 +1,115 @@
+
+
+ 4.0.0
+
+ org.springframework
+ spring-flo-sample-si
+ 0.0.1.BUILD-SNAPSHOT
+ jar
+
+ spring-flo-sample
+ Spring Flo Sample
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.3.2.RELEASE
+
+
+
+
+
+
+
+ org.webjars.bower
+ codemirror
+ 5.1.0
+
+
+ org.webjars.bower
+ angular
+ 1.3.8
+
+
+ org.webjars.bower
+ jshint
+ 2.8.0
+
+
+ org.webjars.bower
+ requirejs
+ 2.1.18
+
+
+ org.webjars.bower
+ jquery
+ 2.2.0
+
+
+ org.webjars.bower
+ lodash
+ 3.10.1
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.webjars
+ webjars-locator
+ 0.31
+
+
+ org.webjars.bower
+ requirejs-domready
+ 2.0.1
+
+
+ org.webjars.bower
+ requirejs-text
+ 2.0.15
+
+
+ org.webjars.bower
+ jointjs
+ 0.9.7
+
+
+ org.webjars.bower
+ json5
+ 0.4.0
+
+
+ org.webjars.bower
+ spring-flo
+ 0.5.0
+
+
+ org.webjars.bower
+ joint
+
+
+
+
+
+
+ UTF-8
+ org.springframework.flo.Application
+ 1.7
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/samples/spring-flo-si/src/main/java/org/springframework/flo/Application.java b/samples/spring-flo-si/src/main/java/org/springframework/flo/Application.java
new file mode 100755
index 0000000..df6784f
--- /dev/null
+++ b/samples/spring-flo-si/src/main/java/org/springframework/flo/Application.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+package org.springframework.flo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+/**
+ *
+ * @author Andy Clement
+ * @author Alex Boyko
+ */
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+// @Bean
+// public WebMvcConfigurer corsConfigurer() {
+// return new WebMvcConfigurerAdapter() {
+// @Override
+// public void addCorsMappings(CorsRegistry registry) {
+// registry.addMapping("/").allowedOrigins("http://localhost:9000");
+// }
+// };
+// }
+}
diff --git a/samples/spring-flo-si/src/main/resources/application.properties b/samples/spring-flo-si/src/main/resources/application.properties
new file mode 100644
index 0000000..8d51d0c
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/application.properties
@@ -0,0 +1 @@
+server.port=8082
\ No newline at end of file
diff --git a/samples/spring-flo-si/src/main/resources/static/css/flosi.css b/samples/spring-flo-si/src/main/resources/static/css/flosi.css
new file mode 100644
index 0000000..dcf5a95
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/css/flosi.css
@@ -0,0 +1,189 @@
+
+.header {
+ font-weight: 400;
+ font-family: "Roboto",sans-serif;
+ font-size: 36px;
+ color: #ffffff;
+ padding: 2px;
+ background-color: #283E49;
+ border: none;
+/* border-top: 4px solid #6db33f; */
+ z-index: 1;
+}
+
+.input-label-div {
+ font-family: "Roboto",sans-serif;
+ font-size: 18px;
+ color: #ffffff;
+ display: inline-block;
+}
+
+#labelpath {
+ width: 200px;
+}
+
+#refreshrate {
+ width: 100px;
+}
+
+.inputfield {
+ font-family: "Roboto",sans-serif;
+ font-size: 18px;
+}
+
+#endpoint {
+ font-family: "Roboto",sans-serif;
+ font-size: 18px;
+ width: 400px;
+}
+
+#endpoint-button {
+ font-family: "Roboto",sans-serif;
+ font-size: 18px;
+ height:80px;
+}
+
+body {
+ background-color: #283E49;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+.control-button {
+ width: 16px;
+ height: 16px;
+}
+
+.header-small {
+ font: 300 24px "Helvetica Neue";
+}
+
+pre {
+ font-size: 18px;
+}
+
+.border-selected {
+ stroke: #34302d;
+ stroke-width: 3;
+}
+
+.controls {
+ border-radius: 2px;
+ border: solid;
+ border-color: #283E49;
+ padding: 5px;
+ margin-top: 3px;
+ background-color: #283E49;
+ border-width: 1px;
+}
+
+.button {
+ color: #ffffff;
+ background-image: none;
+ border-radius: 2px;
+ background-color: #00B0A7;
+ font-size: 18px;
+ line-height: 14px;
+ font-family: "Roboto",sans-serif;
+ border: 2px solid #00B0A7;
+ padding: 5px 20px;
+ text-shadow: none;
+}
+
+.button span {
+ background-color: #34302d;
+ background-image: none;
+ border-radius: 2px;
+ color: #f1f1f1;
+ font-size: 14px;
+ line-height: 14px;
+ font-family: Montserrat,sans-serif;
+ border: 2px solid #6db33f;
+ padding: 5px 20px;
+ text-shadow: none;
+}
+
+.button input {
+ background-color: #34302d;
+ background-image: none;
+ color: #f1f1f1;
+ font-size: 14px;
+ font-family: Montserrat,sans-serif;
+ text-shadow: none;
+ border: 0px;
+ text-align:right;
+}
+
+button.off {
+ background-color: #00B0A7;
+ color: #283E49;
+}
+
+.flow-definition-container {
+ display: none;
+ border: 1px solid;
+ border-color: #283E49;
+ border-radius: 2px;
+ margin-top: 3px;
+ background-color: #ffffff;
+ font-family: monospace;
+ z-index: 2;
+ width:100%;
+ height:100px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+textarea:input {
+ outline: none;
+ border: 1px solid #6db33f;
+}
+
+textarea:input:focus {
+ outline: none;
+ border: 1px solid #000000;
+}
+.canvas {
+ border-color: #283E49;
+}
+.flow-definition {
+ border: 5px;
+ height:100%;
+ width:100%;
+ font-size: 16px;
+ resize: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+/* The text label on the nodes */
+
+.label {
+ font-family: 'Ubuntu Mono';
+ font-size: 12px;
+ color: black;
+}
+
+
+/* The class for the 'icon/unicode_char' on the nodes */
+.label2 {
+ font-size: 18px;
+}
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+ display: none !important;
+}
+
+.box {
+ stroke: #283e49;
+}
+
+
+.node-tooltip-option-name {
+ font-family: 'Ubuntu Mono', monospace;
+}
diff --git a/samples/spring-flo-si/src/main/resources/static/icons/delete.svg b/samples/spring-flo-si/src/main/resources/static/icons/delete.svg
new file mode 100644
index 0000000..5b3f988
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/icons/delete.svg
@@ -0,0 +1,4 @@
+
diff --git a/samples/spring-flo-si/src/main/resources/static/icons/error.svg b/samples/spring-flo-si/src/main/resources/static/icons/error.svg
new file mode 100644
index 0000000..c6c8388
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/icons/error.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/samples/spring-flo-si/src/main/resources/static/icons/rotate.svg b/samples/spring-flo-si/src/main/resources/static/icons/rotate.svg
new file mode 100644
index 0000000..9248e1b
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/icons/rotate.svg
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/samples/spring-flo-si/src/main/resources/static/index.html b/samples/spring-flo-si/src/main/resources/static/index.html
new file mode 100755
index 0000000..2f3f8a7
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/index.html
@@ -0,0 +1,41 @@
+
+
+
+ Spring Integration Viewer
+
+
+
+
+
+
+
+
+
+
+ Spring Integration Viewer
+
+
+ Spring Integration Graph endpoint:
+
+
+ Link label path:
+
+ Refresh rate (ms):
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/spring-flo-si/src/main/resources/static/js/editor-service.js b/samples/spring-flo-si/src/main/resources/static/js/editor-service.js
new file mode 100644
index 0000000..0fa4e11
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/editor-service.js
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+define(function(require) {
+ 'use strict';
+
+ var joint = require('joint');
+
+ return ['$log', function($log) {
+
+ function createHandles(flo, createHandle, element) {
+ var bbox = element.getBBox();
+ var pt = bbox.origin().offset(bbox.width + 3, bbox.height + 3);
+ createHandle(element, 'remove', flo.deleteSelectedNode, pt);
+ }
+
+ function validatePort(/*flo, cellView, magnet*/) {
+ return true;
+ }
+
+ function validateLink(flo, cellViewS, magnetS, cellViewT, magnetT/*, end, linkView*/) {
+ // Prevent linking from input ports.
+ if (magnetS && magnetS.getAttribute('type') === 'input') {
+ return false;
+ }
+ // Prevent linking from output ports to input ports within one element.
+ if (cellViewS === cellViewT) {
+ return false;
+ }
+ // Prevent linking to input ports.
+ if (magnetT && magnetT.getAttribute('type') === 'output') {
+ return false;
+ }
+ return cellViewS.model && cellViewT.model && !(cellViewS.model instanceof joint.shapes.flo.ErrorDecoration) && !(cellViewT.model instanceof joint.shapes.flo.ErrorDecoration);
+ }
+
+ function preDelete(flo, cell) {
+ repairDamage(flo, cell);
+ }
+
+ function handleNodeDropping(flo, dragDescriptor) {
+ // this is a viewer not an editor, so do not adjust the graph structure
+ }
+
+ function calculateDragDescriptor(flo, draggedView, targetUnderMouse, point, context) {
+ // check if it's a tap being dragged
+ var source = draggedView.model;
+ if ((targetUnderMouse instanceof joint.dia.Element) && source.attr('metadata/name') === 'tap') { // jshint ignore:line
+ return {
+ context: context,
+ source: {
+ cell: draggedView.model,
+ },
+ target: {
+ cell: targetUnderMouse,
+ }
+ };
+ }
+
+ // Find closest port
+ var range = 30;
+ var graph = flo.getGraph();
+ var paper = flo.getPaper();
+ var closestData;
+ var minDistance = Number.MAX_VALUE;
+ var maxIcomingLinks = draggedView.model.attr('metadata/constraints/maxIncomingLinksNumber');
+ var maxOutgoingLinks = draggedView.model.attr('metadata/constraints/maxOutgoingLinksNumber');
+ var hasIncomingPort = typeof maxIcomingLinks !== 'number' || maxIcomingLinks > 0;
+ var hasOutgoingPort = typeof maxOutgoingLinks !== 'number' || maxOutgoingLinks > 0;
+ if (!hasIncomingPort && !hasOutgoingPort) {
+ return;
+ }
+ var elements = graph.findModelsInArea(joint.g.rect(point.x - range, point.y - range, 2 * range, 2 * range)); // jshint ignore:line
+ if (Array.isArray(elements)) {
+ elements.forEach(function(model) {
+ var view = paper.findViewByModel(model);
+ if (view && view !== draggedView && model instanceof joint.dia.Element) { // jshint ignore:line
+ var targetMaxIcomingLinks = view.model.attr('metadata/constraints/maxIncomingLinksNumber');
+ var targetMaxOutgoingLinks = view.model.attr('metadata/constraints/maxOutgoingLinksNumber');
+ var targetHasIncomingPort = typeof targetMaxIcomingLinks !== 'number' || targetMaxIcomingLinks > 0;
+ var targetHasOutgoingPort = typeof targetMaxOutgoingLinks !== 'number' || targetMaxOutgoingLinks > 0;
+ if (view.model.attr('metadata/constraints/xorSourceSink')) {
+ if (targetHasIncomingPort) {
+ targetHasIncomingPort = targetHasIncomingPort && graph.getConnectedLinks(view.model, { outbound: true }).length === 0;
+ }
+ if (targetHasOutgoingPort) {
+ targetHasOutgoingPort = targetHasOutgoingPort && graph.getConnectedLinks(view.model, { inbound: true }).length === 0;
+ }
+ }
+ if (draggedView.model.attr('metadata/constraints/xorSourceSink')) {
+ if (hasIncomingPort) {
+ targetHasOutgoingPort = targetHasOutgoingPort && graph.getConnectedLinks(view.model, { outbound: true }).length === 0;
+ }
+ if (hasOutgoingPort) {
+ targetHasIncomingPort = targetHasIncomingPort && graph.getConnectedLinks(view.model, { inbound: true }).length === 0;
+ }
+ }
+ view.$('[magnet]').each(function(index, magnet) {
+ var type = magnet.getAttribute('type');
+ if ((type === 'input' && targetHasIncomingPort && hasOutgoingPort) || (type === 'output' && targetHasOutgoingPort && hasIncomingPort)) {
+ var bbox = joint.V(magnet).bbox(false, paper.viewport); // jshint ignore:line
+ var distance = point.distance({
+ x: bbox.x + bbox.width / 2,
+ y: bbox.y + bbox.height / 2
+ });
+ if (distance < range && distance < minDistance) {
+ minDistance = distance;
+ closestData = {
+ context: context,
+ source: {
+ cell: draggedView.model,
+ selector: type === 'output' ? '.input-port' : '.output-port'
+ },
+ target: {
+ cell: model,
+ selector: '.' + type+'-port'
+ },
+ range: minDistance
+ };
+ }
+ }
+ });
+ }
+ });
+ }
+ if (closestData) {
+ return closestData;
+ }
+
+ // Check if drop on a link is allowed
+ if (targetUnderMouse instanceof joint.dia.Link && !(source.attr('metadata/constraints/xorSourceSink') || source.attr('metadata/constraints/maxOutgoingLinksNumber') === 0 || source.attr('metadata/constraints/maxIncomingLinksNumber') === 0) && graph.getConnectedLinks(source).length === 0) { // jshint ignore:line
+ return {
+ context: context,
+ source: {
+ cell: source
+ },
+ target: {
+ cell: targetUnderMouse
+ }
+ };
+ }
+
+ return {
+ context: context,
+ source: {
+ cell: source
+ },
+ };
+ }
+
+ function validateNode(flo, element) {
+ var errors = [];
+ var graph = flo.getGraph();
+ var constraints = element.attr('metadata/constraints');
+ if (constraints) {
+ var incoming = graph.getConnectedLinks(element, {inbound: true});
+ var outgoing = graph.getConnectedLinks(element, {outbound: true});
+ if (typeof constraints.maxIncomingLinksNumber === 'number' || typeof constraints.minIncomingLinksNumber === 'number') {
+ if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming.length) {
+ if (constraints.maxIncomingLinksNumber === 0) {
+ errors.push({
+ message: 'Sources must appear at the start of a stream',
+ range: element.attr('range')
+ });
+ } else {
+ errors.push({
+ message: 'Max allowed number of incoming links is ' + constraints.maxIncomingLinksNumber,
+ range: element.attr('range')
+ });
+ }
+ }
+ if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming.length) {
+ errors.push({
+ message: 'Min allowed number of incoming links is ' + constraints.minIncomingLinksNumber,
+ range: element.attr('range')
+ });
+ }
+ }
+ if (typeof constraints.maxOutgoingLinksNumber === 'number' || typeof constraints.minOutgoingLinksNumber === 'number') {
+ if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing.length) {
+ if (constraints.maxOutgoingLinksNumber === 0) {
+ errors.push({
+ message: 'Sinks must appear at the end of a stream',
+ range: element.attr('range')
+ });
+ } else {
+ errors.push({
+ message: 'Max allowed number of outgoing links is ' + constraints.maxOutgoingLinksNumber,
+ range: element.attr('range')
+ });
+ }
+ }
+ if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing.length) {
+ errors.push({
+ message: 'Min allowed number of outgoing links is ' + constraints.minOutgoingLinksNumber,
+ range: element.attr('range')
+ });
+ }
+ }
+ if (constraints.xorSourceSink && incoming.length && outgoing.length) {
+ errors.push({
+ message: 'Node can either have incoming or outgoing links, but not both',
+ range: element.attr('range')
+ });
+ }
+ }
+ if (!element.attr('metadata') || element.attr('metadata/unresolved')) {
+ var msg = 'Unknown element \'' + element.attr('metadata/name') + '\'';
+ if (element.attr('metadata/group')) {
+ msg += ' from group \'' + element.attr('metadata/group') + '\'.';
+ }
+ errors.push({
+ message: msg,
+ range: element.attr('range')
+ });
+ }
+
+ // If possible, verify the properties specified match those allowed on this type of element
+ // propertiesRanges are the ranges for each property included the entire '--name=value'.
+ // The format of a range is {'start':{'ch':NNNN,'line':NNNN},'end':{'ch':NNNN,'line':NNNN}}
+ var propertiesRanges = element.attr('propertiesranges');
+ if (propertiesRanges) {
+ var moduleSchema = element.attr('metadata');
+ // Grab the list of supported properties for this module type
+ moduleSchema.get('properties').then(function(moduleSchemaProperties) {
+ if (!moduleSchemaProperties) {
+ moduleSchemaProperties = {};
+ }
+ // Example moduleSchemaProperties:
+ // {"host":{"name":"host","type":"String","description":"the hostname of the mail server","defaultValue":"localhost","hidden":false},
+ // "password":{"name":"password","type":"String","description":"the password to use to connect to the mail server ","defaultValue":null,"hidden":false}
+ var specifiedProperties = element.attr('props');
+ Object.keys(specifiedProperties).forEach(function(propertyName) {
+ if (!moduleSchemaProperties[propertyName]) {
+ // The schema does not mention that property
+ var propertyRange = propertiesRanges[propertyName];
+ if (propertyRange) {
+ errors.push({
+ message: 'unrecognized option \''+propertyName+'\' for module \''+element.attr('metadata/name')+'\'',
+ range: propertyRange
+ });
+ }
+ }
+ });
+ });
+ }
+
+ return errors;
+ }
+
+ function moveNodeOnNode(flo, node, pivotNode, side, shouldRepairDamage) {
+ side = side || 'left';
+ if (canSwap(flo, node, pivotNode, side)) {
+ var link;
+ var i;
+ if (side === 'left') {
+ var sources = [];
+ if (shouldRepairDamage) {
+ /*
+ * Commented out because it doesn't prevent cycles.
+ */
+// if (graph.getConnectedLinks(pivotNode, {inbound: true}).length > 0 || graph.getConnectedLinks(node, {outbound: true}).length > 0) {
+ repairDamage(flo, node);
+// }
+ }
+ var pivotTargetLinks = flo.getGraph().getConnectedLinks(pivotNode, {inbound: true});
+ for (i = 0; i < pivotTargetLinks.length; i++) {
+ link = pivotTargetLinks[i];
+ sources.push(link.get('source').id);
+ link.remove();
+ }
+ for (i = 0; i < sources.length; i++) {
+ flo.createLink({
+ 'id': sources[i],
+ 'selector': '.output-port'
+ }, {
+ 'id': node.id,
+ 'selector': '.input-port'
+ });
+ }
+ flo.createLink({
+ 'id': node.id,
+ 'selector': '.output-port'
+ }, {
+ 'id': pivotNode.id,
+ 'selector': '.input-port'
+ });
+ } else if (side === 'right') {
+ var targets = [];
+ if (shouldRepairDamage) {
+ /*
+ * Commented out because it doesn't prevent cycles.
+ */
+// if (graph.getConnectedLinks(pivotNode, {outbound: true}).length > 0 || graph.getConnectedLinks(node, {inbound: true}).length > 0) {
+ repairDamage(flo, node);
+// }
+ }
+ var pivotSourceLinks = flo.getGraph().getConnectedLinks(pivotNode, {outbound: true});
+ for (i = 0; i < pivotSourceLinks.length; i++) {
+ link = pivotSourceLinks[i];
+ targets.push(link.get('target').id);
+ link.remove();
+ }
+ for (i = 0; i < targets.length; i++) {
+ flo.createLink({
+ 'id': node.id,
+ 'selector': '.output-port'
+ }, {
+ 'id': targets[i],
+ 'selector': '.input-port'
+ });
+ }
+ flo.createLink({
+ 'id': pivotNode.id,
+ 'selector': '.output-port'
+ }, {
+ 'id': node.id,
+ 'selector': '.input-port'
+ });
+ }
+ }
+ }
+
+ function moveNodeOnLink(flo, node, link, shouldRepairDamage) {
+ var source = link.get('source').id;
+ var target = link.get('target').id;
+
+ if (shouldRepairDamage) {
+ repairDamage(flo, node);
+ }
+ link.remove();
+
+ if (source) {
+ flo.createLink({
+ 'id': source,
+ 'selector': '.output-port'
+ }, {
+ 'id': node.id,
+ 'selector': '.input-port'
+ });
+ }
+ if (target) {
+ flo.createLink({
+ 'id': node.id,
+ 'selector': '.output-port'
+ }, {
+ 'id': target,
+ 'selector': '.input-port'
+ });
+ }
+ }
+
+ function repairDamage(flo, node) {
+ /*
+ * remove incoming, outgoing links and cache their sources and targets not equal to current node
+ */
+ var sources = [];
+ var targets = [];
+ var i = 0;
+ var links = flo.getGraph().getConnectedLinks(node);
+ for (i = 0; i < links.length; i++) {
+ var targetId = links[i].get('target').id;
+ var sourceId = links[i].get('source').id;
+ if (targetId === node.id) {
+ links[i].remove();
+ sources.push(sourceId);
+ } else if (sourceId === node.id) {
+ links[i].remove();
+ targets.push(targetId);
+ }
+ }
+ /*
+ * best attempt to connect source and targets bypassing the node
+ */
+ if (sources.length === 1) {
+ var source = sources[0];
+ for (i = 0; i < targets.length; i++) {
+ flo.createLink({
+ 'id': source,
+ 'selector': '.output-port'
+ }, {
+ 'id': targets[i],
+ 'selector': '.input-port'
+ });
+ }
+ } else if (targets.length === 1) {
+ var target = targets[0];
+ for (i = 0; i < sources.length; i++) {
+ flo.createLink({
+ 'id': sources[i],
+ 'selector': '.output-port'
+ }, {
+ 'id': target,
+ 'selector': '.input-port'
+ });
+ }
+ }
+ }
+
+ /**
+ * Check if node being dropped and drop target node next to each other such that they won't be swapped by the drop
+ */
+ function canSwap(flo, dropee, target, side) {
+ var i, targetId, sourceId, noSwap = (dropee.id === target.id);
+ if (dropee === target) {
+ $log.debug('What!??? Dragged == Dropped!!! id = ' + target);
+ }
+ var links = flo.getGraph().getConnectedLinks(dropee);
+ for (i = 0; i < links.length && !noSwap; i++) {
+ targetId = links[i].get('target').id;
+ sourceId = links[i].get('source').id;
+ noSwap = (side === 'left' && targetId === target.id && sourceId === dropee.id) || (side === 'right' && targetId === dropee.id && sourceId === target.id);
+ }
+ return !noSwap;
+ }
+
+ return {
+ 'createHandles': createHandles,
+ 'validatePort': validatePort,
+ 'validateLink': validateLink,
+ 'calculateDragDescriptor': calculateDragDescriptor,
+ 'handleNodeDropping': handleNodeDropping,
+ 'validateNode': validateNode,
+ 'preDelete': preDelete,
+ 'interactive': {
+ 'vertexAdd': false
+ },
+ 'allowLinkVertexEdit': false
+ };
+
+ }];
+
+});
diff --git a/samples/spring-flo-si/src/main/resources/static/js/flosi-app.js b/samples/spring-flo-si/src/main/resources/static/js/flosi-app.js
new file mode 100644
index 0000000..25278fa
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/flosi-app.js
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+define(function(require) {
+ 'use strict';
+ var angular = require('angular');
+ require('flo');
+ var app = angular.module('floSiApp', [ 'spring.flo' ]);
+ app.factory('SampleMetamodelService', require('metamodel-service'));
+ app.factory('SampleRenderService', require('render-service'));
+ app.factory('SampleEditorService', require('editor-service'));
+
+ app.controller('SiController', ['$scope', '$http', 'SampleMetamodelService', function($scope, $http, metamodelService) {
+ $scope.endpoint = 'http://localhost:8080/integration';
+ $scope.labelpath = "stats.sendcount";
+ $scope.refreshrate=0;
+
+ var refreshTimer;
+
+ $scope.load = function(endpoint) {
+ console.log("Loading from endpoint: '"+endpoint+"'");
+ $scope.endpoint = endpoint;
+ // Load the graph from the endpoint
+ $http.get(endpoint, { }).success(function(json) {
+ // console.log("JSON is "+json);
+ // console.log("it is "+$('#endpoint').val());
+ // $('#flow-definition').val('foo');
+ $scope.definition.text = JSON.stringify(json);
+ $scope.flo.updateGraphRepresentation();
+ }).error(function(err) {
+ console.log(err);
+ });
+ };
+
+ $scope.updateRefreshRate = function(newRefreshRate) {
+ console.log("Update refresh rate: '"+newRefreshRate+"'");
+ $scope.refreshrate=newRefreshRate;
+ if (refreshTimer) {
+ clearTimeout(refreshTimer);
+ }
+ if (newRefreshRate >0) {
+ if (newRefreshRate < 250) {
+ $scope.refreshrate = 250;
+ }
+ var refresher = function() {
+ refresh();
+ refreshTimer = setTimeout(function() { refresher() }, $scope.refreshrate);
+ }
+ refreshTimer = setTimeout(refresher, $scope.refreshrate);
+ } else {
+ $scope.refreshrate=0;
+ }
+ }
+
+ function refresh() {
+ $http.get($scope.endpoint, { }).success(function(json) {
+ metamodelService.updateGraphLabels($scope.flo, JSON.stringify(json), $scope.labelpath);
+ }).error(function(err) {
+ console.log(err);
+ });
+ }
+
+ $scope.updateLabelPath = function(newLabelPath) {
+ console.log("Update label path: '"+newLabelPath+"'");
+ $scope.labelpath = newLabelPath;
+ // Update the graph from the endpoint
+ $http.get($scope.endpoint, { }).success(function(json) {
+ metamodelService.updateGraphLabels($scope.flo, JSON.stringify(json), newLabelPath);
+ }).error(function(err) {
+ console.log(err);
+ });
+ };
+
+ }]).directive('ngEnter', function() {
+ return function(scope, element, attrs) {
+ element.bind("keydown keypress", function(event) {
+ if(event.which === 13) {
+ scope.$apply(function(){
+ scope.$eval(attrs.ngEnter);
+ });
+ event.preventDefault();
+ }
+ });
+ };
+ });
+ return app;
+});
diff --git a/samples/spring-flo-si/src/main/resources/static/js/graph-to-text.js b/samples/spring-flo-si/src/main/resources/static/js/graph-to-text.js
new file mode 100644
index 0000000..cb375bb
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/graph-to-text.js
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * Convert a graph to a text representation.
+ *
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+define(function() {
+ 'use strict';
+
+ // Graph
+ var g;
+
+ // Number of Links left to visit
+ var numberOfLinksToVisit;
+
+ // Number of nodes left to visit
+ var numberOfNodesToVisit;
+
+ // Map of links left to visit indexed by id
+ var linksToVisit;
+
+ // Map of nodes left to visit indexed by id
+ var nodesToVisit;
+
+ // Map of nodes incoming non-visited links degrees index by node id
+ var nodesInDegrees;
+
+ // Priority:
+ // 1. find links whose source has no other links pointing at it
+ // 2. find links whose source has already been processed (not currently needed in sample DSL since
+ // can't create graphs like that due to metamodel constraints)
+ // 3. find remaining links
+ function nextLink() {
+ var indegree = Number.MAX_INT;
+ var currentBest;
+ for (var id in linksToVisit) {
+ var link = g.getCell(id);
+ var source = g.getCell(link.get('source').id);
+ var currentInDegree = nodesInDegrees[source.get('id')];
+ if (currentInDegree === 0) {
+ return visit(link);
+ } else if (indegree > currentInDegree) {
+ indegree = currentInDegree;
+ currentBest = link;
+ }
+ }
+ if (currentBest) {
+ return visit(currentBest);
+ }
+ }
+
+ function visit(e) {
+ if (e.isLink()) {
+ delete linksToVisit[e.get('id')];
+ nodesInDegrees[e.get('target').id]--;
+ numberOfLinksToVisit--;
+ } else {
+ delete nodesToVisit[e.get('id')];
+ numberOfNodesToVisit--;
+ }
+ return e;
+ }
+
+ function init(graph) {
+ numberOfLinksToVisit = 0;
+ numberOfNodesToVisit = 0;
+ linksToVisit = {};
+ nodesToVisit = {};
+ nodesInDegrees = {};
+ g = graph;
+ g.getElements().forEach(function(element) {
+ if (element.attr('metadata/name')) { // is it a node?
+ nodesToVisit[element.get('id')] = element;
+ var indegree = 0;
+ g.getConnectedLinks(element, {inbound: true}).forEach(function(link) {
+ if (link.get('source') && link.get('source').id && g.getCell(link.get('source').id) &&
+ g.getCell(link.get('source').id).attr('metadata/name')) {
+ linksToVisit[link.get('id')] = link;
+ numberOfLinksToVisit++;
+ indegree++;
+ }
+ });
+ nodesInDegrees[element.get('id')] = indegree;
+ numberOfNodesToVisit++;
+ }
+ });
+ }
+
+ /**
+ * Starts at a link and proceeds down a chain. Converts each node to
+ * text and then joins them with a ' > '.
+ */
+ function chainToText(link) {
+ var text = '';
+ var source = g.getCell(link.get('source').id);
+ text += nodeToText(source, true);
+ while (link) {
+ var target = g.getCell(link.get('target').id);
+ text += ' > ';
+ text += nodeToText(target, false);
+
+ // Find next not visited link to follow
+ link = null;
+ var outgoingLinks = g.getConnectedLinks(target, {outbound: true});
+ for (var i = 0; i < outgoingLinks.length && !link; i++) {
+ if (linksToVisit[outgoingLinks[i].get('id')]) {
+ source = target;
+ link = visit(outgoingLinks[i]);
+ }
+ }
+ }
+ return text;
+ }
+
+ /**
+ * Very basic format. From a node to the text:
+ * "name --key=value --key=value"
+ */
+ function nodeToText(element) {
+ var text = '';
+ var props = element.attr('props');
+ if (!element) {
+ return;
+ }
+ text += element.attr('metadata/name');
+ if (props) {
+ Object.keys(props).forEach(function(propertyName) {
+ text += ' --' + propertyName + '=' + props[propertyName];
+ });
+ }
+ visit(element);
+ return text;
+ }
+
+ function appendChainText(text, chainText) {
+ if (chainText) {
+ if (text) {
+ text += '\n';
+ }
+ text += chainText;
+ }
+ return text;
+ }
+
+ // Translate a graph into a basic string
+ return function(g) {
+ var text = '';
+ var chainText;
+ var id;
+ init(g);
+ while (numberOfLinksToVisit) {
+ chainText = chainToText(nextLink());
+ text = appendChainText(text, chainText);
+ }
+ // Visit all disconnected nodes
+ for (id in nodesToVisit) {
+ chainText = nodeToText(nodesToVisit[id], true);
+ text = appendChainText(text, chainText);
+ }
+ return text;
+ };
+
+});
\ No newline at end of file
diff --git a/samples/spring-flo-si/src/main/resources/static/js/main.js b/samples/spring-flo-si/src/main/resources/static/js/main.js
new file mode 100755
index 0000000..24733bd
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/main.js
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+requirejs.config({
+ baseUrl:'js',
+ paths: {
+ joint: '/webjars/jointjs/dist/joint',
+ backbone: '/webjars/backbone/backbone',
+ domReady: '/webjars/requirejs-domready/domReady',
+ angular: '/webjars/angular/angular',
+ jquery: '/webjars/jquery/dist/jquery',
+ bootstrap:'/webjars/bootstrap/bootstrap',
+ lodash: '/webjars/lodash/lodash', // lodash.compat
+ dagre: '/webjars/dagre/dist/dagre.core',
+ graphlib: '/webjars/graphlib/graphlib.core',
+ text : '/webjars/requirejs-text/text',
+ flo : '/webjars/spring-flo/dist/spring-flo',
+ json5 : '/webjars/json5/json5'
+ },
+ map: {
+ '*': {
+ // Backbone requires underscore. This forces requireJS to load lodash instead:
+ 'underscore': 'lodash'
+ }
+ },
+ packages: [
+ {
+ name: 'codemirror',
+ location: '../lib/codemirror',
+ main: 'lib/codemirror'
+ }
+ ],
+ shim: {
+ angular: {
+ deps: ['bootstrap'],
+ exports: 'angular'
+ },
+ bootstrap: {
+ deps: ['jquery']
+ },
+ graphlib: {
+ deps: ['underscore']
+ },
+ dagre: {
+ deps: ['graphlib', 'underscore']
+ },
+ joint: {
+ deps: ['jquery', 'underscore', 'backbone'],
+ },
+ underscore: {
+ exports: '_'
+ },
+ 'flo': {
+ deps: ['angular', 'jquery', 'joint', 'underscore']
+ }
+ }
+});
+
+define(['require','angular'], function (require, angular) {
+ 'use strict';
+ require(['domReady!', 'flosi-app'],
+ function (document) {
+ angular.bootstrap(document, ['floSiApp']);
+ }
+ );
+});
diff --git a/samples/spring-flo-si/src/main/resources/static/js/metamodel-sample.json b/samples/spring-flo-si/src/main/resources/static/js/metamodel-sample.json
new file mode 100644
index 0000000..4313f82
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/metamodel-sample.json
@@ -0,0 +1,90 @@
+[
+{
+ 'name':'general', 'group':'general', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'service-activator', 'group':'messaging-endpoints', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'message-handler', 'group':'messaging-endpoints', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'gateway', 'group':'messaging-endpoints', 'description':'Gateway',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'splitter', 'group':'routing', 'description':'Splitter',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+ }
+,
+{
+ 'name':'router', 'group':'routing', 'description':'Router',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'transformer', 'group':'transformation', 'description':'Transformer',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'bridge', 'group':'routing', 'description':'Bridge',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'channel', 'group':'connectors', 'description':'Channel',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'publish-subscribe-channel', 'group':'connectors', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'test-producer', 'group':'producers', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'BareHandler', 'group':'producers', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+},
+{
+ 'name':'filter', 'group':'routing', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'aggregator', 'group':'routing', 'description':'Produce an output message after correlating some set of incoming messages',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'logging-channel-adapter', 'group':'connectors', 'description':'?',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'chain', 'group':'messaging-endpoints', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'inbound-channel-adapter', 'group':'messaging-endpoints', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+,
+{
+ 'name':'outbound-channel-adapter', 'group':'messaging-endpoints', 'description':'',
+ 'constraints':{ 'maxIncomingLinksNumber':10, 'maxOutgoingLinksNumber':10 }
+}
+]
\ No newline at end of file
diff --git a/samples/spring-flo-si/src/main/resources/static/js/metamodel-service.js b/samples/spring-flo-si/src/main/resources/static/js/metamodel-service.js
new file mode 100644
index 0000000..4f5d7b3
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/metamodel-service.js
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+define(function(require) {
+ 'use strict';
+
+ require('json5');
+
+ var convertGraphToText = require('graph-to-text');
+ var convertTextToGraph = require('text-to-graph');
+ var updateGraph = require('update-graph');
+
+ return ['$http', '$q', '$timeout', '$log', 'MetamodelUtils', function($http, $q, $timeout, $log, metamodelUtils) {
+
+ var metamodel;
+
+ // Internally stored metamodel load promise
+ var request;
+
+ var statsProperties = [
+ {'name':'name','default':'?', 'description':'name'},
+ {'name':'id','default':'?', 'description':'node id'},
+ {'name':'componentType','default':'','description':'Detailed component type'},
+ {'name':'stats.loggingEnabled', 'default': '?', 'description':'?' },
+ {'name':'stats.statsEnabled', 'default': '?', 'description':'?' },
+ {'name':'stats.countsEnabled', 'default': '?', 'description':'?' },
+ {'name':'stats.sendRate.count', 'default': '?', 'description':'?' },
+ {'name':'stats.sendRate.min', 'default': '?', 'description':'?' },
+ {'name':'stats.sendRate.max', 'default': '?', 'description':'?' },
+ {'name':'stats.sendRate.mean', 'default': '?', 'description':'?' },
+ {'name':'stats.sendRate.standardDeviation', 'default': '?', 'description':'?' },
+ {'name':'stats.sendRate.countLong', 'default': '?', 'description':'?' },
+ {'name':'stats.errorRate.count', 'default': '?', 'description':'?' },
+ {'name':'stats.errorRate.min', 'default': '?', 'description':'?' },
+ {'name':'stats.errorRate.max', 'default': '?', 'description':'?' },
+ {'name':'stats.errorRate.mean', 'default': '?', 'description':'?' },
+ {'name':'stats.errorRate.standardDeviation', 'default': '?', 'description':'?' },
+ {'name':'stats.errorRate.countLong', 'default': '?', 'description':'?' },
+ {'name':'stats.sendCount', 'default': '?', 'description':'?' },
+ {'name':'stats.sendErrorCount', 'default': '?', 'description':'?' },
+ {'name':'stats.timeSinceLastSend', 'default': '?', 'description':'?' },
+ {'name':'stats.meanSendRate', 'default': '?', 'description':'?' },
+ {'name':'stats.meanErrorRate', 'default': '?', 'description':'?' },
+ {'name':'stats.meanErrorRatio', 'default': '?', 'description':'?' },
+ {'name':'stats.meanSendDuration', 'default': '?', 'description':'?' },
+ {'name':'stats.minSendDuration', 'default': '?', 'description':'?' },
+ {'name':'stats.maxSendDuration', 'default': '?', 'description':'?' },
+ {'name':'stats.standardDeviationSendDuration', 'default': '?', 'description':'?' },
+ {'name':'stats.sendDuration.count', 'default': '?', 'description':'?' },
+ {'name':'stats.sendDuration.min', 'default': '?', 'description':'?' },
+ {'name':'stats.sendDuration.max', 'default': '?', 'description':'?' },
+ {'name':'stats.sendDuration.mean', 'default': '?', 'description':'?' },
+ {'name':'stats.sendDuration.standardDeviation', 'default': '?', 'description':'?' },
+ {'name':'stats.sendDuration.countLong', 'default': '?', 'description':'?' }];
+
+ /**
+ * Helper that goes from basic JSON to a lazy getter structure. Useful when the
+ * metamodel is 'cheap' to build. If it is costly to discover the actual properties
+ * the getter may be more complex (e.g. make a REST request).
+ */
+ function createMetadata(entry) {
+ var props = {};
+ if (!entry.properties) {
+ // use the default stats properties
+ entry.properties = JSON.parse(JSON.stringify(statsProperties));
+ }
+ if (Array.isArray(entry.properties)) {
+ entry.properties.forEach(function(property) {
+ if (!property.id) {
+ property.id = property.name;
+ }
+ props[property.id] = property;
+ });
+ }
+ entry.properties = props;
+ return {
+ name: entry.name,
+ group: entry.group,
+ icon: entry.icon,
+ constraints: entry.constraints,
+ description: entry.description,
+ metadata: entry.metadata,
+ properties: entry.properties,
+ get: function(property) {
+ var deferred = $q.defer();
+ if (entry.hasOwnProperty(property)) {
+ deferred.resolve(entry[property]);
+ } else {
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+ };
+ }
+
+ function load() {
+ // COULDDO: to cache the result here, check result before doing this processing
+ // and simply return it if it is set. If doing that may want to override refresh
+ // in this service
+ var metamodelData = JSON5.parse(require('text!metamodel-sample.json'));
+ var deferred = $q.defer();
+ var newData = {};
+ metamodelData.forEach(function(data) {
+ var metadata = createMetadata(data);
+ if (!newData[metadata.group]) {
+ newData[metadata.group] = {};
+ }
+ newData[metadata.group][metadata.name] = metadata;
+ });
+ metamodel = newData;
+ deferred.resolve(metamodel);
+ request = deferred.promise;
+ return request;
+ }
+
+ function graphToText(flo, definition) {
+ definition.text = convertGraphToText(flo.getGraph());
+ }
+
+ function updateGraphLabels(flo, text, labelpath) {
+ updateGraph(text, flo.getGraph(), labelpath);
+ }
+
+ function textToGraph(flo, definition) {
+ // TODO perhaps push these flo operations into the 'caller' to make this simpler
+ flo.getGraph().clear();
+ load().then(function(metamodel) {
+ convertTextToGraph(definition.text, flo, metamodel, metamodelUtils);
+ updateGraph(definition.text,flo.getGraph(),'stats.sendcount');
+ flo.performLayout();
+ flo.fitToPage();
+ });
+ }
+
+ return {
+ 'load': load,
+ 'textToGraph': textToGraph,
+ 'updateGraphLabels': updateGraphLabels,
+ 'graphToText': graphToText
+ };
+
+ }];
+
+});
diff --git a/samples/spring-flo-si/src/main/resources/static/js/render-service.js b/samples/spring-flo-si/src/main/resources/static/js/render-service.js
new file mode 100644
index 0000000..e54d5af
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/render-service.js
@@ -0,0 +1,746 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+define(function(require) {
+ 'use strict';
+
+ var joint = require('joint');
+
+ var dagre = require('dagre');
+
+ var HANDLE_ICON_MAP = {
+ 'remove': 'icons/delete.svg',
+ };
+
+ var DECORATION_ICON_MAP = {
+ 'error': 'icons/error.svg'
+ };
+
+ var IMAGE_W = 120,
+ IMAGE_H = 40;
+
+ var HORIZONTAL_PADDING = 10;
+
+ joint.shapes.si = {};
+
+ joint.routers.floforintegration = (function() {
+
+ // expands a box by specific value
+ function expand(bbox, val) {
+ return joint.g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val });
+ }
+
+ function routeAround(exp, ref, anchor, opt) {
+
+ var anchorSide = exp.sideNearestToPoint(anchor);
+ var expAnchor = exp.pointNearestToPoint(anchor);
+ var line = joint.g.line(ref, anchor);
+ var center = exp.center();
+ var intersection;
+ var pts = [];
+
+ if (anchorSide !== 'top') {
+ intersection = line.intersection(joint.g.line(exp.origin(), exp.topRight()));
+ if (intersection) {
+ if (anchorSide === 'bottom') {
+ if (intersection.x - exp.x + expAnchor.x - exp.x <= exp.width) {
+ pts = [exp.origin(), exp.bottomLeft()];
+ } else {
+ pts = [exp.topRight(), exp.corner()];
+ }
+ } else {
+ if (anchorSide === 'left') {
+ pts = [exp.origin()];
+ } else {
+ pts = [exp.topRight()];
+ }
+ }
+ if (opt.includeExtraPoints) {
+ pts.push(expAnchor);
+ }
+ return pts;
+ }
+ }
+
+ if (anchorSide !== 'bottom') {
+ intersection = line.intersection(joint.g.line(exp.corner(), exp.bottomLeft()));
+ if (intersection) {
+ if (anchorSide === 'top') {
+ if (intersection.x - exp.x + expAnchor.x - exp.x <= exp.width) {
+ pts = [exp.bottomLeft(), exp.origin()];
+ } else {
+ pts = [exp.corner(), exp.topRight()];
+ }
+ } else {
+ if (anchorSide === 'left') {
+ pts = [exp.bottomLeft()];
+ } else {
+ pts = [exp.corner()];
+ }
+ }
+ if (opt.includeExtraPoints) {
+ pts.push(expAnchor);
+ }
+ return pts;
+ }
+ }
+
+ if (anchorSide !== 'left') {
+ intersection = line.intersection(joint.g.line(exp.origin(), exp.bottomLeft()));
+ if (intersection) {
+ if (anchorSide === 'right') {
+ if (intersection.y - exp.y + expAnchor.y - exp.y <= exp.height) {
+ pts = [exp.origin(), exp.topRight()];
+ } else {
+ pts = [exp.bottomLeft(), exp.corner()];
+ }
+ } else {
+ if (anchorSide === 'top') {
+ pts = [exp.origin()];
+ } else {
+ pts = [exp.bottomLeft()];
+ }
+ }
+ if (opt.includeExtraPoints) {
+ pts.push(expAnchor);
+ }
+ return pts;
+ }
+ }
+
+ if (anchorSide !== 'right') {
+ intersection = line.intersection(joint.g.line(exp.topRight(), exp.corner()));
+ if (intersection) {
+ if (anchorSide === 'left') {
+ if (intersection.y - exp.y + expAnchor.y - exp.y <= exp.height) {
+ pts = [exp.topRight(), exp.origin()];
+ } else {
+ pts = [exp.corner(), exp.bottomLeft()];
+ }
+ } else {
+ if (anchorSide === 'top') {
+ pts = [exp.topRight()];
+ } else {
+ pts = [exp.corner()];
+ }
+ }
+ if (opt.includeExtraPoints) {
+ pts.push(expAnchor);
+ }
+ return pts;
+ }
+ }
+
+ return pts;
+ }
+
+ function findRoute(vx, opt, linkView) {
+
+ var vertices = opt.metro ? joint.routers.metro(vx, opt, linkView) : vx;
+ var sourceRoute = [], targetRoute = [];
+
+ var paper = linkView.paper;
+ var reference = vertices.length ? vertices[0] : joint.g.rect(linkView.targetBBox).center();
+ var sourceAnchorPt = paper.options.linkConnectionPoint(linkView, linkView.sourceView, linkView.sourceMagnet, reference);
+ var targetAnchorPt = paper.options.linkConnectionPoint(linkView, linkView.targetView, linkView.targetMagnet, sourceAnchorPt);
+ var padding = opt.elementPadding || 20;
+
+ if (linkView.sourceView) {
+ var expSource = expand(linkView.sourceView.model.getBBox(), padding);
+ while (vertices.length && expSource.containsPoint(vertices[0])) {
+ vertices.splice(0, 1);
+ }
+ var sourceRef = vertices.length ? joint.g.point(vertices[0]) : targetAnchorPt;
+ sourceRoute = routeAround(expSource, sourceRef, sourceAnchorPt, opt).reverse();
+ }
+ if (linkView.targetView) {
+ var expTarget = expand(linkView.targetView.model.getBBox(), padding);
+ while (vertices.length && expTarget.containsPoint(vertices[vertices.length - 1])) {
+ vertices.splice(vertices.length - 1, 1);
+ }
+ var targetRef = vertices.length ? joint.g.point(vertices[vertices.length - 1]) : sourceAnchorPt;
+
+ targetRoute = routeAround(expTarget, targetRef, targetAnchorPt, opt);
+ }
+
+ return sourceRoute.concat(vertices).concat(targetRoute);
+ };
+
+ return findRoute;
+
+ })();
+
+ joint.shapes.si.Channel = joint.shapes.basic.Generic.extend({
+
+ markup:
+ ''+
+ '' +
+ ''+
+ ''+
+ ''+
+ ''+
+ '' +
+// ''+
+ ''+
+ ''+
+ '',
+
+ defaults: joint.util.deepSupplement({
+ type: 'channel',//joint.shapes.flo.NODE_TYPE,
+ position: {x: 0, y: 0},
+ size: { width: 100, height: 40 },
+ attrs: {
+ '.': {
+ magnet: false,
+ },
+ '.the_shape': {
+ 'stroke':'#000000'
+ },
+ // rounded edges around image
+// '.border': {
+// width: IMAGE_W,
+// height: IMAGE_H,
+// rx: 2,
+// ry: 2,
+// 'fill-opacity':0, // see through
+// stroke: '#eeeeee',
+// 'stroke-width': 0,
+// },
+// '.box': {
+// width: IMAGE_W,
+// height: IMAGE_H,
+// rx: 2,
+// ry: 2,
+// //'fill-opacity':0, // see through
+// stroke: '#6db33f',
+// fill: '#eeeeee',
+// 'stroke-width': 2,
+// },
+ '.input-port': {
+ type: 'input',
+ port: 'input',
+ r:4,
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#eeeeee',
+ transform: 'translate(' + -2 + ',' + ((20/2)-2+10) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1,
+ },
+ '.output-port': {
+ type: 'output',
+ port: 'output',
+ r:4,
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#eeeeee',
+ transform: 'translate(' + (100+8-2) + ',' + ((20/2)-2+10) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1,
+ },
+// '.tap-port': {
+// type: 'output',
+// port: 'tap',
+// r: 4,
+// magnet: true,
+// fill: '#eeeeee',
+// 'ref-x': 0.5,
+// 'ref-y': 0.99999999,
+// ref: '.border',
+// stroke: '#34302D'
+// },
+ '.label': {
+ 'ref-x': 0.5, // jointjs specific: relative position to ref'd element
+ 'ref-y': 0.525,
+ 'y-alignment': 'middle',
+ 'x-alignment' : 'middle',
+ ref: '.the_shape', // jointjs specific: element for ref-x, ref-y
+ fill: 'black',
+ 'stroke': 'black',
+ 'font-size': '12px',
+ 'font-family': 'Ubuntu Mono',
+ 'color': 'black'
+ },
+ '.label2': {
+ 'y-alignment': 'middle',
+ 'ref-x': HORIZONTAL_PADDING+2, // jointjs specific: relative position to ref'd element
+ 'ref-y': 0.55, // jointjs specific: relative position to ref'd element
+ ref: '.border', // jointjs specific: element for ref-x, ref-y
+ fill: 'black',
+ 'font-size': 20
+ },
+// '.stream-label': {
+// 'x-alignment': 'middle',
+// 'y-alignment': -0.999999,
+// 'ref-x': 0.5, // jointjs specific: relative position to ref'd element
+// 'ref-y': 0, // jointjs specific: relative position to ref'd element
+// ref: '.border', // jointjs specific: element for ref-x, ref-y
+// fill: '#AAAAAA',
+// 'font-size': 15
+// },
+// '.shape': {
+// }
+ }
+ }, joint.shapes.basic.Generic.prototype.defaults)
+ });
+
+
+ joint.shapes.si.Node = joint.shapes.basic.Generic.extend({
+ markup:
+ ''+
+ '' +
+ '' +
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ '',
+
+ defaults: joint.util.deepSupplement({
+
+ type: 'node',//joint.shapes.flo.NODE_TYPE,
+ position: {x: 0, y: 0},
+ size: { width: IMAGE_W, height: IMAGE_H },
+ attrs: {
+ '.': { magnet: false },
+ // rounded edges around image
+ '.border': {
+ width: IMAGE_W,
+ height: IMAGE_H,
+ rx: 3,
+ ry: 3,
+ 'fill-opacity':0, // see through
+ stroke: '#eeeeee',
+ 'stroke-width': 0
+ },
+
+ '.box': {
+ width: IMAGE_W,
+ height: IMAGE_H,
+ rx: 3,
+ ry: 3,
+ //'fill-opacity':0, // see through
+ stroke: '#6db33f',
+ fill: '#eeeeee',
+ 'stroke-width': 1
+ },
+ '.input-port': {
+ type: 'input',
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#eeeeee',
+ transform: 'translate(' + -2 + ',' + ((IMAGE_H/2)-2) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1
+ },
+ '.output-port': {
+ type: 'output',
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#eeeeee',
+ transform: 'translate(' + (IMAGE_W-2) + ',' + ((IMAGE_H/2)-2) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1
+ },
+ '.error-port': {
+ type: 'output',
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#ff0000',
+ transform: 'translate(' + (IMAGE_W/2-2) + ',' + ((IMAGE_H)-2) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1
+ },
+ '.label': {
+ 'text-anchor': 'middle',
+ 'ref-x': 0.5, // jointjs specific: relative position to ref'd element
+ // 'ref-y': -12, // jointjs specific: relative position to ref'd element
+ 'ref-y': 0.3,
+ ref: '.border', // jointjs specific: element for ref-x, ref-y
+ fill: 'black',
+ 'font-size': 14
+ },
+ '.label2': {
+ 'text': '\u21d2',
+ 'text-anchor': 'middle',
+ 'ref-x': 0.15, // jointjs specific: relative position to ref'd element
+ 'ref-y': 0.15, // jointjs specific: relative position to ref'd element
+ ref: '.border', // jointjs specific: element for ref-x, ref-y
+ transform: 'translate(' + (IMAGE_W/2) + ',' + (IMAGE_H/2) + ')',
+ fill: 'black',
+ 'font-size': 24
+ },
+ '.shape': {
+ },
+ '.image': {
+ width: IMAGE_W,
+ height: IMAGE_H
+ }
+ }
+ }, joint.shapes.basic.Generic.prototype.defaults)
+ });
+
+ joint.shapes.si.ServiceActivator = joint.shapes.basic.Generic.extend({
+ markup:
+ ''+
+ '' +
+ '' +
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''+
+ ''
+ ,
+
+
+ defaults: joint.util.deepSupplement({
+
+ type: 'node',//joint.shapes.flo.NODE_TYPE,
+ position: {x: 0, y: 0},
+ size: { width: IMAGE_W, height: IMAGE_H },
+ attrs: {
+ '.': { magnet: false },
+ // rounded edges around image
+ '.border': {
+ width: IMAGE_W,
+ height: IMAGE_H,
+ rx: 3,
+ ry: 3,
+ 'fill-opacity':0, // see through
+ stroke: '#eeeeee',
+ 'stroke-width': 0
+ },
+ '.arrowGray': {
+ 'stroke':'#aaaaaa',
+ 'stroke-width':'2',
+ 'fill': '#aaaaaa'
+ },
+ '.arrowBlack': {
+ 'stroke':'black',
+ 'stroke-width':'2',
+ 'fill': 'black'
+ },
+ '.blockSolid': {
+ 'stroke':'#000000',
+ 'stroke-width':'2',
+ 'fill': 'black'
+ },
+ '.blockEmpty': {
+ 'stroke':'#000000',
+ 'stroke-width':'2',
+ 'fill': 'white'
+ },
+ '.box': {
+ width: IMAGE_W,
+ height: IMAGE_H,
+ rx: 3,
+ ry: 3,
+ //'fill-opacity':0, // see through
+ stroke: '#6db33f',
+ fill: '#eeeeee',
+ 'stroke-width': 1
+ },
+ '.input-port': {
+ type: 'input',
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#eeeeee',
+ transform: 'translate(' + -2 + ',' + ((IMAGE_H/2)-2) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1
+ },
+ '.output-port': {
+ type: 'output',
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#eeeeee',
+ transform: 'translate(' + (IMAGE_W-2) + ',' + ((IMAGE_H/2)-2) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1
+ },
+ '.error-port': {
+ type: 'output',
+ height: 4, width: 4,
+ magnet: true,
+ fill: '#ff0000',
+ transform: 'translate(' + (IMAGE_W/2-2) + ',' + ((IMAGE_H)-2) + ')',
+ stroke: '#34302d',
+ 'stroke-width': 1
+ },
+ '.label': {
+ 'text-anchor': 'middle',
+ 'ref-x': 0.5, // jointjs specific: relative position to ref'd element
+// 'ref-y': -12, // jointjs specific: relative position to ref'd element
+ 'ref-y': 0.3,
+ ref: '.border', // jointjs specific: element for ref-x, ref-y
+ fill: 'black',
+ 'font-size': 14
+ },
+ '.label2': {
+ 'text': '\u21d2',
+ 'text-anchor': 'middle',
+ 'ref-x': 0.15, // jointjs specific: relative position to ref'd element
+ 'ref-y': 0.15, // jointjs specific: relative position to ref'd element
+ ref: '.border', // jointjs specific: element for ref-x, ref-y
+ transform: 'translate(' + (IMAGE_W/2) + ',' + (IMAGE_H/2) + ')',
+ fill: 'black',
+ 'font-size': 24
+ },
+ '.shape': {
+ },
+ '.image': {
+ width: IMAGE_W,
+ height: IMAGE_H
+ }
+ }
+ }, joint.shapes.basic.Generic.prototype.defaults)
+ });
+
+
+ return ['$log', function($log) {
+
+ function createHandle(kind) {
+ return new joint.shapes.flo.ErrorDecoration({
+ size: {width: 10, height: 10},
+ attrs: {
+ 'image': {
+ 'xlink:href': HANDLE_ICON_MAP[kind]
+ }
+ }
+ });
+ }
+
+ function createDecoration(kind) {
+ return new joint.shapes.flo.ErrorDecoration({
+ size: {width: 16, height: 16},
+ attrs: {
+ 'image': {
+ 'xlink:href': DECORATION_ICON_MAP[kind]
+ }
+ }
+ });
+ }
+
+ function createNode(metadata, props) {
+ if (metadata.name === 'channel' || metadata.name === 'publish-subscribe-channel') {
+ return new joint.shapes.si.Channel();
+ } else if (metadata.name === 'service-activator') {
+ return new joint.shapes.si.ServiceActivator();
+ } else {
+ return new joint.shapes.si.Node();
+ }
+ }
+
+ function initializeNewNode(node, context) {
+ var metadata = node.attr('metadata');
+ if (metadata) {
+ node.attr('.label/text', node.attr('metadata/name'));
+ if (node.attr('metadata/constraints/maxIncomingLinksNumber') === 0) {
+ node.attr('.input-port/display','none');
+ }
+ if (node.attr('metadata/constraints/maxOutgoingLinksNumber') === 0) {
+ node.attr('.output-port/display','none');
+ }
+
+ var type = node.attr('metadata/name');
+ if (type === 'tap') {
+ if (!node.attr('props/channel')) {
+ node.attr('props/channel', 'tap:stream:STREAM');
+ }
+ refreshVisuals(node, 'props/channel', context.paper);
+ } else if (type === 'named-channel') {
+ // Default channel for named channel is 'queue:default'
+ if (!node.attr('props/channel')) {
+ node.attr('props/channel', 'queue:default');
+ }
+ refreshVisuals(node, 'props/channel', context.paper);
+ }
+ }
+ node.attr('.label2/text','');
+
+ }
+
+ function validateNode(flo, node) {
+ return [];
+ }
+
+ function fitLabel(paper, node, labelPath) {
+ var label = node.attr(labelPath);
+ if (label && label.length<9) {
+ return;
+ }
+ var view = paper.findViewByModel(node);
+ if (view && label) {
+ var textView = view.findBySelector(labelPath.substr(0, labelPath.indexOf('/')))[0];
+ var offset = 0;
+ if (node.attr('.label2/text')) {
+ var label2View = view.findBySelector('.label2')[0];
+ if (label2View) {
+ var box = joint.V(label2View).bbox(false, paper.viewport);
+ offset = HORIZONTAL_PADDING + box.width;
+ }
+ }
+ var width = joint.V(textView).bbox(false, paper.viewport).width;
+ var threshold = IMAGE_W - HORIZONTAL_PADDING - HORIZONTAL_PADDING - offset;
+ if (offset) {
+ node.attr('.label1/ref-x', Math.max((offset + HORIZONTAL_PADDING + width / 2) / IMAGE_W, 0.5), { silent: true });
+ }
+ // Trim package prefix
+
+ if (!label.endsWith('?')) {
+// console.log("modifying label "+label);
+ // Sample name: com.foo.method(a.b.c.Order)
+ var openParen = label.indexOf('(');
+ if (openParen !== -1) {
+ label = label.substring(0,openParen);
+ }
+ width = joint.V(textView).bbox(false, paper.viewport).width;
+ if (width > threshold) {
+ var lastDot = label.lastIndexOf('.');
+ if (lastDot !== -1) {
+ label = label.substring(lastDot+1);
+ }
+ console.log('driving label change');
+ node.attr(labelPath, label, { silent: true });
+ view.update();
+ width = joint.V(textView).bbox(false, paper.viewport).width;
+ for (var i = 1; i < label.length && width > threshold; i++) {
+ node.attr(labelPath, label.substr(0, label.length - i) + '\u2026', { silent: true });
+ view.update();
+ width = joint.V(textView).bbox(false, paper.viewport).width;
+ if (offset) {
+ node.attr('.label1/ref-x', Math.max((offset + HORIZONTAL_PADDING + width / 2) / IMAGE_W, 0.5), { silent: true });
+ }
+ }
+ }
+ }
+// view.update();
+ }
+ }
+
+ function createLink() {
+ var link = new joint.shapes.flo.Link(joint.util.deepSupplement({
+ router: { name: 'floforintegration', args: {elementPadding: 20/*, metro: true*/} },
+ connector: { name: 'smooth' },
+ attrs: {
+ '.': {
+ //filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } }
+ },
+ '.connection': { 'stroke-width': 3, 'stroke': 'black', 'stroke-linecap': 'round' },
+ '.marker-arrowheads': { display: 'none' },
+ '.tool-options': { display: 'none' },
+ 'stroke':'red' // TODO necessary?
+ },
+ }, joint.shapes.flo.Link.prototype.defaults));
+ return link;
+ }
+
+ function isSemanticProperty(propertyPath) {
+ return propertyPath === '.label/text';
+ }
+
+ function refreshVisuals(element, changedPropertyPath, paper) {
+ fitLabel(paper, element, '.label/text');
+ }
+
+ function layout(paper) {
+ var graph = paper.model;
+ var i;
+ var g = new dagre.graphlib.Graph();
+ g.setGraph({});
+ g.setDefaultEdgeLabel(function() {return{};});
+
+ var nodes = graph.getElements();
+ for (i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+ if (node.get('type') === joint.shapes.flo.NODE_TYPE) {
+ g.setNode(node.id, node.get('size'));
+ }
+ }
+
+ var links = graph.getLinks();
+ for (i = 0; i < links.length; i++) {
+ var link = links[i];
+ if (link.get('type') === joint.shapes.flo.LINK_TYPE) {
+ var options = {
+ minlen: 1.5
+ };
+// if (link.get('labels') && link.get('labels').length > 0) {
+// options.minlen = 1 + link.get('labels').length * 0.5;
+// }
+ g.setEdge(link.get('source').id, link.get('target').id, options);
+ link.set('vertices', []);
+ }
+ }
+
+ g.graph().rankdir = 'LR';
+ dagre.layout(g);
+ g.nodes().forEach(function(v) {
+ var node = graph.getCell(v);
+ if (node) {
+ var bbox = node.getBBox();
+ node.translate(g.node(v).x - bbox.x, g.node(v).y - bbox.y);
+ }
+ });
+ }
+
+ function getLinkAnchorPoint(linkView, view, magnet, reference) {
+ if (magnet) {
+ var cssClass = magnet.getAttribute('class');
+ var bbox = joint.V(magnet).bbox(false, linkView.paper.viewport);
+ var rect = joint.g.rect(bbox);
+ if (cssClass.indexOf('input-port') !== -1) {
+ return joint.g.point(rect.x, rect.y + rect.height / 2);
+ } else if (cssClass.indexOf('error-port') !== -1) {
+ return joint.g.point(rect.x + rect.width / 2, rect.y + rect.height);
+ } else {
+ return joint.g.point(rect.x + rect.width, rect.y + rect.height / 2);
+ }
+ } else {
+ $log.debug('No magnet!');
+ return reference;
+ }
+ }
+
+ return {
+ 'createHandle': createHandle,
+ 'createDecoration': createDecoration,
+ 'createNode': createNode,
+ 'createLink': createLink,
+ 'initializeNewNode': initializeNewNode,
+ 'isSemanticProperty': isSemanticProperty,
+ 'refreshVisuals': refreshVisuals,
+ 'layout': layout,
+ 'getLinkAnchorPoint': getLinkAnchorPoint
+ };
+
+ }];
+
+});
diff --git a/samples/spring-flo-si/src/main/resources/static/js/text-to-graph.js b/samples/spring-flo-si/src/main/resources/static/js/text-to-graph.js
new file mode 100644
index 0000000..8a7bf91
--- /dev/null
+++ b/samples/spring-flo-si/src/main/resources/static/js/text-to-graph.js
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * Convert a text representation to a graph.
+ *
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+define(function(require) {
+ 'use strict';
+ var joint = require('joint');
+
+ function collapseOneLevel(prefix, obj, collector) {
+ var type = typeof obj;
+ if (obj == null) {
+ collector[prefix] = null;
+ return;
+ }
+ if (type === 'object') {
+ Object.keys(obj).forEach(function(key) {
+ collapseOneLevel(prefix.length==0?key:prefix+'.'+key,obj[key],collector);
+ });
+ } else if (type === 'array') {
+ for (var i=0;i0.975) p = 0;
+ setTimeout(function() {animate(link,p)},25);
+ }
+ }
+
+
+ return function(input, flo, metamodel, metamodelUtils) {
+ // input is a string like this (3 nodes: foo, goo and hoo): foo --a=b --c=d > goo --d=e --f=g>hoo
+ var trimmed = input.trim();
+ if (trimmed.length===0) {
+ return;
+ }
+ var getMetadata = function(type) {
+ var group = metamodelUtils.matchGroup(metamodel, type, 1, 1);
+ var md = metamodelUtils.getMetadata(metamodel, type, group);
+ if (!md || md.unresolved) {
+ var secondAttempt;
+ // Examples: mail:outbound-channel-adapter or file:inbound-channel-adapter
+ if (type.indexOf("inbound-channel-adapter")!=-1) {
+ type = "inbound-channel-adapter";
+ group = metamodelUtils.matchGroup(metamodel, type, 1, 1);
+ secondAttempt = metamodelUtils.getMetadata(metamodel, type, group);
+ if (secondAttempt && !secondAttempt.unresolved) {
+ md = secondAttempt;
+ }
+ } else if (type.indexOf("outbound-channel-adapter")!=-1) {
+ type = "outbound-channel-adapter";
+ group = metamodelUtils.matchGroup(metamodel, type, 1, 1);
+ secondAttempt = metamodelUtils.getMetadata(metamodel, type, group);
+ if (secondAttempt && !secondAttempt.unresolved) {
+ md = secondAttempt;
+ }
+ } else {
+ // use the general one - this will ensure validation is OK and tooltips work but
+ // we aren't really sure what type it is.
+ type = 'general';
+ group = metamodelUtils.matchGroup(metamodel, type, 1, 1);
+ secondAttempt = metamodelUtils.getMetadata(metamodel, type, group);
+ if (secondAttempt && !secondAttempt.unresolved) {
+ md = secondAttempt;
+ }
+ }
+ }
+ return md;
+ }
+ var integrationGraph = JSON.parse(input);
+ var nodes = integrationGraph.nodes;
+ var nodesMap = {};
+ for (var i=0;i');
+// var lastNode = null;
+// for (var e=0;e goo --d=e --f=g>hoo
+ var trimmed = input.trim();
+ if (trimmed.length===0) {
+ return;
+ }
+ var integrationGraph = JSON.parse(input);
+ var nodes = integrationGraph.nodes;
+ var graphNodes = graph.getElements();
+ var map = {};
+ var linksToVisit = graph.getLinks();
+ graphNodes.forEach(function(element) {
+ if (element.attr('metadata/name')) { // is it a node?
+// if (!element.get('source')) {
+ map[element.attr('props/id')] = element;
+ } else {
+ linksToVisit.push(element);
+ }
+ });
+ function toLabel(text) {
+ var string = text.toString();
+ if (string.length>5) {
+ string = string.substring(0,5);
+ if (string.endsWith('.')) {
+ string = string.substring(0,4);
+ }
+ return string;
+ }
+ return text;
+ }
+ // Go through nodes, updating properties
+ for (var i=0;i