blob: 7a1c2fb71f6b0a3f3e14d2dba116ebe2e00f074f [file] [log] [blame]
/**
********************************************************************************
* 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();
}
}