/** | |
******************************************************************************** | |
* Copyright (c) 2020-2022 DLR e. V. and OFFIS e. V. | |
* | |
* This program and the accompanying materials are made | |
* available under the terms of the Eclipse Public License 2.0 | |
* which is available at https://www.eclipse.org/legal/epl-2.0/ | |
* | |
* SPDX-License-Identifier: EPL-2.0 | |
* | |
* Contributors: | |
* OFFIS e. V. - initial API and implementation | |
* DLR e. V. - initial API and implementation | |
*******************************************************************************/ | |
package org.eclipse.app4mc.amalthea.visualization.runnabledependency; | |
import java.io.IOException; | |
import java.io.InputStreamReader; | |
import java.io.OutputStreamWriter; | |
import java.io.PrintWriter; | |
import java.lang.ProcessBuilder.Redirect; | |
import java.nio.charset.StandardCharsets; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.function.Consumer; | |
import javax.annotation.PostConstruct; | |
import org.eclipse.app4mc.amalthea.model.SWModel; | |
import org.eclipse.app4mc.visualization.ui.registry.Visualization; | |
import org.eclipse.core.runtime.Platform; | |
import org.eclipse.e4.core.di.extensions.Preference; | |
import org.eclipse.e4.core.services.events.IEventBroker; | |
import org.eclipse.jface.dialogs.MessageDialog; | |
import org.eclipse.jface.layout.GridDataFactory; | |
import org.eclipse.jface.layout.GridLayoutFactory; | |
import org.eclipse.jface.layout.RowLayoutFactory; | |
import org.eclipse.swt.SWT; | |
import org.eclipse.swt.browser.Browser; | |
import org.eclipse.swt.browser.LocationListener; | |
import org.eclipse.swt.events.SelectionEvent; | |
import org.eclipse.swt.events.SelectionListener; | |
import org.eclipse.swt.widgets.Button; | |
import org.eclipse.swt.widgets.Composite; | |
import org.eclipse.swt.widgets.Display; | |
import org.eclipse.swt.widgets.FileDialog; | |
import org.eclipse.swt.widgets.Shell; | |
import org.osgi.service.component.annotations.Component; | |
import net.sourceforge.plantuml.eclipse.utils.PlantumlConstants; | |
@Component(property = { | |
"name=Runnable Data Dependencies", | |
"description=Runnable Label Access Visualization for Software Models" | |
}) | |
public class RunnableDependencyVisualization implements Visualization { | |
private static GraphvizGeneratorConfig config = new GraphvizGeneratorConfig(); | |
private GraphvizGenerator graphvizGenerator; | |
private Browser browser; | |
private String dotPath; | |
private SWModel model; | |
/** | |
* Entry point for the visualization framework | |
* | |
* @param model software model that shall be visualized | |
* @param parent parent component | |
* @param dotPath path to Graphviz DOT executable | |
* @param broker event broker for element selection | |
*/ | |
@PostConstruct | |
public void createVisualization( | |
SWModel model, | |
Composite parent, | |
@Preference( | |
nodePath = "net.sourceforge.plantuml.eclipse", | |
value = PlantumlConstants.GRAPHVIZ_PATH) | |
String dotPath, | |
IEventBroker broker) { | |
this.graphvizGenerator = new GraphvizGenerator(config, true, true); | |
this.model = model; | |
if (dotPath == null) { | |
if (Platform.getOS().equals(Platform.OS_WIN32)) { | |
this.dotPath = "dot.exe"; | |
} else { | |
this.dotPath = "dot"; | |
} | |
} else { | |
this.dotPath = dotPath; | |
} | |
Composite pane = new Composite(parent, SWT.NONE); | |
GridLayoutFactory.fillDefaults().applyTo(pane); | |
Composite buttonArea = new Composite(pane, SWT.NONE); | |
this.browser = new Browser(pane, SWT.NONE); | |
if (broker != null) { | |
setupElementNavigation(broker); | |
} | |
addToggleButton(buttonArea, "Horizontal Layout", config::setHorizontalLayout, config.isHorizontalLayout()); | |
addToggleButton(buttonArea, "Show Labels", config::setShowLabels, config.isShowLabels()); | |
addToggleButton(buttonArea, "Show R/W dependencies", config::setShowLabelDependencies, config.isShowLabelDependencies()); | |
addToggleButton(buttonArea, "Show Control Flow", config::setShowCallDependencies, config.isShowCallDependencies()); | |
addToggleButton(buttonArea, "Show Tasks", config::setShowTasks, config.isShowTasks()); | |
addPushButton(buttonArea, "Export", () -> { | |
FileDialog fd = new FileDialog(parent.getShell(), SWT.SAVE); | |
fd.setFilterNames(new String[] { "Scalable Vector Graphics (*.svg)", "Portable Document Format (*.pdf)", | |
"Portable Network Graphics (*.png)", "all files (*.*)" }); | |
fd.setFilterExtensions(new String[] { "*.svg", "*.pdf", "*.png", "*.*" }); | |
String path = fd.open(); | |
if (path != null) { | |
export(path); | |
} | |
}); | |
addPushButton(buttonArea, "+", () -> browser.execute( | |
"svg=document.getElementsByTagName(\"svg\")[0]; svg.width.baseVal.value *=1.25; svg.height.baseVal.value *=1.25;")); | |
addPushButton(buttonArea, "-", () -> browser.execute( | |
"svg=document.getElementsByTagName(\"svg\")[0]; svg.width.baseVal.value /=1.25; svg.height.baseVal.value /=1.25;")); | |
RowLayoutFactory.swtDefaults().applyTo(buttonArea); | |
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(browser); | |
visualize(); | |
} | |
/** | |
* Setup navigation to a selected element in the model viewer | |
* | |
* @param broker | |
*/ | |
private void setupElementNavigation(IEventBroker broker) { | |
browser.addLocationListener(LocationListener.changingAdapter(c -> { | |
c.doit = true; | |
Object target = null; | |
int idx = c.location.indexOf('#'); | |
if (idx >= 0) { | |
target = graphvizGenerator.getObjectById(c.location.substring(idx + 1)); | |
} | |
if (target != null) { | |
HashMap<String, Object> data = new HashMap<>(); | |
data.put("modelElements", Collections.singletonList(target)); | |
broker.send("org/eclipse/app4mc/amalthea/editor/SELECT", data); | |
} | |
})); | |
} | |
/** | |
* Helper for adding a toggle button | |
* | |
* @param parent container element | |
* @param text label | |
* @param f button select action; takes the button's selection | |
* status as an argument | |
* @param initialSelected initial selection state of the button | |
*/ | |
private void addToggleButton(Composite parent, String text, final Consumer<Boolean> f, boolean initialSelected) { | |
final Button btn = new Button(parent, SWT.TOGGLE); | |
btn.setText(text); | |
btn.setSelection(initialSelected); | |
btn.addSelectionListener(new SelectionListener() { | |
@Override | |
public void widgetDefaultSelected(SelectionEvent evt) { | |
widgetSelected(evt); | |
} | |
@Override | |
public void widgetSelected(SelectionEvent evt) { | |
f.accept(btn.getSelection()); | |
visualize(); | |
} | |
}); | |
} | |
/** | |
* Helper for adding a push button | |
* | |
* @param parent container element | |
* @param text label | |
* @param cmd button select action | |
*/ | |
private void addPushButton(Composite parent, String text, Runnable cmd) { | |
final Button btn = new Button(parent, SWT.PUSH); | |
btn.setText(text); | |
btn.addSelectionListener(new SelectionListener() { | |
@Override | |
public void widgetDefaultSelected(SelectionEvent evt) { | |
widgetSelected(evt); | |
} | |
@Override | |
public void widgetSelected(SelectionEvent evt) { | |
cmd.run(); | |
} | |
}); | |
} | |
/** | |
* Visualizes a given model element in a browser. The dot graph is constructed | |
* and compiled in a separate thread. | |
*/ | |
private void visualize() { | |
new Thread(() -> { | |
String dot = graphvizGenerator.createDot(model); | |
String result; | |
try { | |
result = runGraphviz(dot, "-Tsvg"); | |
} catch (IOException e) { | |
result = prepareErrorMessage(e); | |
Platform.getLog(RunnableDependencyVisualization.class).error(result, e); | |
} | |
if (result != null && !browser.isDisposed()) { | |
final String browserContent = result; | |
browser.getDisplay().asyncExec(() -> { | |
if (!browser.isDisposed()) { | |
browser.setText(browserContent); | |
} | |
}); | |
} | |
}).start(); | |
} | |
private String prepareErrorMessage(IOException e) { | |
return "Error invoking Graphviz: \"" + e.getMessage() | |
+ "\". Make sure you have configured the path to the dot executable properly in the PlantUML preferences."; | |
} | |
/** | |
* Export the visualization | |
* | |
* @param path export destination | |
*/ | |
private void export(String path) { | |
Display display = Display.getCurrent(); | |
final Shell shell = display != null ? display.getActiveShell() : null; | |
new Thread(() -> { | |
String ext = path.substring(path.lastIndexOf('.') + 1); | |
final String dot = new GraphvizGenerator(config, !"dot".equals(ext), false).createDot(model); | |
try { | |
if ("dot".equals(ext)) { | |
try (PrintWriter writer = new PrintWriter(path)) { | |
writer.append(dot); | |
} | |
} else { | |
runGraphviz(dot, "-T" + ext, "-o", path); | |
} | |
} catch (IOException e) { | |
String msg = prepareErrorMessage(e); | |
Platform.getLog(RunnableDependencyVisualization.class).error(msg, e); | |
MessageDialog.openError(shell, "Error during export", msg); | |
} | |
}).start(); | |
} | |
/** | |
* Execute the DOT command, pass the model to stdin and return stdout | |
* | |
* @param dot the DOT model to be provided on stdin | |
* @param args arguments to the dot command | |
* @return the command output | |
*/ | |
private String runGraphviz(String dot, String... args) throws IOException { | |
List<String> command = new ArrayList<>(args.length + 1); | |
command.add(dotPath); | |
command.addAll(Arrays.asList(args)); | |
Process process = new ProcessBuilder(command).redirectError(Redirect.PIPE).start(); | |
OutputStreamWriter writer = new OutputStreamWriter(process.getOutputStream()); | |
InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8); | |
writer.append(dot); | |
writer.close(); | |
StringBuilder sb = new StringBuilder(); | |
char[] buf = new char[1024]; | |
int len; | |
while ((len = reader.read(buf)) >= 0) { | |
sb.append(buf, 0, len); | |
} | |
return sb.toString(); | |
} | |
} |