/******************************************************************************* * Copyright (c) 2007, 2020 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.debug.ui; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.Array; import java.net.URL; import java.nio.charset.Charset; import java.util.Arrays; import java.util.LinkedList; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.resource.ColorDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTError; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.browser.LocationAdapter; import org.eclipse.swt.browser.LocationEvent; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DropTarget; import org.eclipse.swt.dnd.DropTargetAdapter; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.simantics.databoard.type.Datatype; import org.simantics.databoard.util.ObjectUtils; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.common.ResourceArray; import org.simantics.db.common.procedure.adapter.DisposableListener; import org.simantics.db.common.request.UnaryRead; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.SelectionHints; import org.simantics.db.layer0.request.PossibleURI; import org.simantics.db.layer0.request.ResourceURIToVariable; import org.simantics.db.layer0.request.VariableURI; import org.simantics.db.layer0.variable.AbstractChildVariable; import org.simantics.db.layer0.variable.AbstractPropertyVariable; import org.simantics.db.layer0.variable.Variable; import org.simantics.db.layer0.variable.VariableNode; import org.simantics.db.layer0.variable.Variables; import org.simantics.db.service.SerialisationSupport; import org.simantics.debug.ui.internal.Activator; import org.simantics.layer0.Layer0; import org.simantics.structural2.variables.Connection; import org.simantics.structural2.variables.VariableConnectionPointDescriptor; import org.simantics.ui.dnd.LocalObjectTransfer; import org.simantics.ui.dnd.ResourceReferenceTransfer; import org.simantics.ui.dnd.ResourceTransferUtils; import org.simantics.ui.utils.ResourceAdaptionUtils; import org.simantics.utils.FileUtils; import org.simantics.utils.bytes.Base64; import org.simantics.utils.ui.ErrorLogger; import org.simantics.utils.ui.ISelectionUtils; import org.simantics.utils.ui.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Antti Villberg * @author Tuukka Lehtonen */ public class VariableDebugger extends Composite { private static final Logger LOGGER = LoggerFactory.getLogger(VariableDebugger.class); public interface HistoryListener { void historyChanged(); } private final static String DEFAULT_DEBUGGER_CSS_FILE = "debugger.css"; //$NON-NLS-1$ private final static String DEFAULT_DEBUGGER_CSS_PATH = "css/" + DEFAULT_DEBUGGER_CSS_FILE; //$NON-NLS-1$ private static int RESOURCE_NAME_MAX_LENGTH = 1000; private final Charset utf8 = Charset.forName("UTF-8"); //$NON-NLS-1$ private final LocalResourceManager resourceManager; private String cssPath; private Text updateTriggerCounter; private Browser browser; private final ColorDescriptor green = ColorDescriptor.createFrom(new RGB(0x57, 0xbc, 0x95)); private final LinkedList backHistory = new LinkedList(); private final LinkedList forwardHistory = new LinkedList(); private String currentElement = null; /** * The Session used to access the graph. Received from outside of this * class and therefore it is not disposed here, just used. */ private final Session session; private final CopyOnWriteArrayList historyListeners = new CopyOnWriteArrayList(); protected Layer0 L0; protected boolean disposed; class PageContentListener extends DisposableListener { int triggerCounter; int updateCount; AtomicReference lastResult = new AtomicReference(); @Override public void execute(final String content) { ++triggerCounter; //System.out.println("LISTENER TRIGGERED: " + triggerCounter); //System.out.println("LISTENER:\n" + content); if (lastResult.getAndSet(content) == null) { if (!disposed) { getDisplay().asyncExec(new Runnable() { @Override public void run() { String content = lastResult.getAndSet(null); if (content == null) return; ++updateCount; //System.out.println("UPDATE " + updateCount); if (!browser.isDisposed()) browser.setText(content); if (!updateTriggerCounter.isDisposed()) updateTriggerCounter.setText(updateCount + "/" + triggerCounter); //$NON-NLS-1$ } }); } } } @Override public void exception(Throwable t) { LOGGER.error("Page content listener failed unexpectedly", t); } } private PageContentListener pageContentListener; /** * @param parent * @param style * @param session * @param resource the initial resource to debug or null for * initially blank UI. */ public VariableDebugger(Composite parent, int style, final Session session, String initialURI) { super(parent, style); Assert.isNotNull(session, "session is null"); //$NON-NLS-1$ this.session = session; this.currentElement = initialURI; this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), parent); initializeCSS(); addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { disposed = true; PageContentListener l = pageContentListener; if (l != null) l.dispose(); } }); } public void defaultInitializeUI() { setLayout(new GridLayout(4, false)); setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); createDropLabel(this); createResourceText(this); createUpdateTriggerCounter(this); Browser browser = createBrowser(this); GridDataFactory.fillDefaults().span(4, 1).grab(true, true).applyTo(browser); } protected void initializeCSS() { // Extract default css to a temporary location if necessary. try { IPath absolutePath = PathUtils.getAbsolutePath(Activator.PLUGIN_ID, DEFAULT_DEBUGGER_CSS_PATH); if (absolutePath != null) { cssPath = absolutePath.toFile().toURI().toString(); } else { File tempDir = FileUtils.getOrCreateTemporaryDirectory(false); File css = new File(tempDir, DEFAULT_DEBUGGER_CSS_FILE); if (!css.exists()) { URL url = FileLocator.find(Activator.getDefault().getBundle(), new Path(DEFAULT_DEBUGGER_CSS_PATH), null); if (url == null) throw new FileNotFoundException("Could not find '" + DEFAULT_DEBUGGER_CSS_PATH + "' in bundle '" + Activator.PLUGIN_ID + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ cssPath = FileUtils.copyResource(url, css, true).toURI().toString(); } else { cssPath = css.toURI().toString(); } } } catch (IOException e) { e.printStackTrace(); // CSS extraction failed, let's just live without it then. ErrorLogger.defaultLogWarning(e); } } public Label createDropLabel(Composite parent) { final Label label = new Label(parent, SWT.BORDER | SWT.FLAT); label.setAlignment(SWT.CENTER); label.setText(Messages.VariableDebugger_DragResourceToDebugger); label.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); GridData data = new GridData(SWT.LEFT, SWT.FILL, false, false); label.setLayoutData(data); // Add resource id drop support to the drop-area. DropTarget dropTarget = new DropTarget(label, DND.DROP_LINK | DND.DROP_COPY); dropTarget.setTransfer(new Transfer[] { TextTransfer.getInstance(), ResourceReferenceTransfer.getInstance(), LocalObjectTransfer.getTransfer() }); dropTarget.addDropListener(new DropTargetAdapter() { @Override public void dragEnter(DropTargetEvent event) { event.detail = DND.DROP_LINK; label.setBackground((Color) resourceManager.get(green)); return; } @Override public void dragLeave(DropTargetEvent event) { label.setBackground(null); } @Override public void drop(DropTargetEvent event) { label.setBackground(null); String uri = null; try { uri = parseUri(event); if (uri == null) { event.detail = DND.DROP_NONE; return; } changeLocation(uri); } catch (DatabaseException e) { LOGGER.error("Changing location to URI {} failed", uri, e); } } private String parseUri(DropTargetEvent event) throws DatabaseException { Variable v = parseVariable(event); String uri = v != null ? session.sync(new VariableURI(v)) : null; if (uri == null) { Resource r = parseResource(event); uri = r != null ? session.sync(new PossibleURI(r)) : null; } return uri; } private Variable parseVariable(DropTargetEvent event) { return ISelectionUtils.getSinglePossibleKey(event.data, SelectionHints.KEY_MAIN, Variable.class); } private Resource parseResource(DropTargetEvent event) throws DatabaseException { ResourceArray[] ra = null; if (event.data instanceof String) { try { SerialisationSupport support = session.getService(SerialisationSupport.class); ra = ResourceTransferUtils.readStringTransferable(support, (String) event.data).toResourceArrayArray(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (DatabaseException e) { e.printStackTrace(); } } else { ra = ResourceAdaptionUtils.toResourceArrays(event.data); } if (ra != null && ra.length > 0) return ra[0].resources[ra[0].resources.length - 1]; return null; } }); return label; } public void createResourceText(Composite parent) { final Text text = new Text(parent, SWT.BORDER); GridData data = new GridData(SWT.FILL, SWT.FILL, true, false); text.setLayoutData(data); Button button = new Button(parent, SWT.NONE); button.setText(Messages.VariableDebugger_Lookup); GridData data2 = new GridData(SWT.FILL, SWT.FILL, false, false); button.setLayoutData(data2); button.addSelectionListener(new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { widgetSelected(e); } @Override public void widgetSelected(SelectionEvent e) { String uri = null; try { uri = text.getText(); // Make sure that URI is resolvable to Variable session.sync(new ResourceURIToVariable(uri)); changeLocation(uri); } catch (DatabaseException e1) { LOGGER.error("Lookup failed for URI {}", uri, e1); } } }); } protected Text createUpdateTriggerCounter(Composite parent) { Text label = new Text(parent, SWT.BORDER | SWT.FLAT); label.setEditable(false); label.setToolTipText(Messages.VariableDebugger_TextToolTip); GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL) .grab(false, false).hint(32, SWT.DEFAULT).applyTo(label); updateTriggerCounter = label; return label; } public Browser createBrowser(Composite parent) { try { browser = new Browser(parent, SWT.NONE); } catch (SWTError e) { //System.out.println("Could not instantiate Browser: " + e.getMessage()); browser = new Browser(parent, SWT.NONE); } browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); // Left/right arrows for back/forward browser.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { // System.out.println("key, char: " + e.keyCode + ", " + (int) e.character + " (" + e.character + ")"); // if (e.keyCode == SWT.BS) { // back(); // } if ((e.stateMask & SWT.ALT) != 0) { if (e.keyCode == SWT.ARROW_RIGHT) forward(); if (e.keyCode == SWT.ARROW_LEFT) back(); } } }); // Add listener for debugging functionality browser.addLocationListener(new LocationAdapter() { @Override public void changing(LocationEvent event) { String location = event.location; if (location.startsWith("simantics:browser")) //$NON-NLS-1$ location = "about:" + location.substring(17); //$NON-NLS-1$ //System.out.println("changing: location=" + location); // Do not follow links that are meant as actions that are // handled below. event.doit = false; if ("about:blank".equals(location)) { //$NON-NLS-1$ // Just changing to the same old blank url is ok since it // allows the browser to refresh itself. event.doit = true; } if (location.startsWith("about:-link")) { //$NON-NLS-1$ String target = location.replace("about:-link", ""); //$NON-NLS-1$ //$NON-NLS-2$ try { byte[] bytes = Base64.decode(target); String url = new String(bytes, utf8); if (url.equals(currentElement)) { event.doit = false; return; } changeLocation(url); } catch (IOException e) { ErrorLogger.defaultLogError(e); } } else if (location.startsWith("about:-remove")) { //$NON-NLS-1$ } else if (location.startsWith("about:-edit-value")) { //$NON-NLS-1$ } } }); // Schedule a request that updates the browser content. refreshBrowser(); return browser; } public void refreshBrowser() { if (currentElement == null) return; // Schedule a request that updates the browser content. if (pageContentListener != null) pageContentListener.dispose(); pageContentListener = new PageContentListener(); session.asyncRequest(new UnaryRead(currentElement) { @Override public String perform(ReadGraph graph) throws DatabaseException { String content = calculateContent(graph, parameter); //System.out.println("HTML: " + content); return content; } }, pageContentListener); } public String getDebuggerLocation() { return currentElement; } public void changeLocation(String url) { if (currentElement != null) { backHistory.addLast(currentElement); } currentElement = url; forwardHistory.clear(); refreshBrowser(); fireHistoryChanged(); } public void addHistoryListener(HistoryListener l) { historyListeners.add(l); } public void removeHistoryListener(HistoryListener l) { historyListeners.remove(l); } private void fireHistoryChanged() { for (HistoryListener l : historyListeners) l.historyChanged(); } public boolean hasBackHistory() { return backHistory.isEmpty(); } public boolean hasForwardHistory() { return forwardHistory.isEmpty(); } public void back() { if (backHistory.isEmpty()) return; forwardHistory.addFirst(currentElement); currentElement = backHistory.removeLast(); refreshBrowser(); fireHistoryChanged(); } public void forward() { if (forwardHistory.isEmpty()) return; backHistory.addLast(currentElement); currentElement = forwardHistory.removeFirst(); refreshBrowser(); fireHistoryChanged(); } protected String toName(Object o) { Class clazz = o.getClass(); if (clazz.isArray()) { int length = Array.getLength(o); if (length > RESOURCE_NAME_MAX_LENGTH) { if (o instanceof byte[]) { byte[] arr = (byte[]) o; byte[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH); return truncated("byte", Arrays.toString(arr2), arr.length); //$NON-NLS-1$ } else if (o instanceof int[]) { int[] arr = (int[]) o; int[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH); return truncated("int", Arrays.toString(arr2), arr.length); //$NON-NLS-1$ } else if (o instanceof long[]) { long[] arr = (long[]) o; long[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH); return truncated("long", Arrays.toString(arr2), arr.length); //$NON-NLS-1$ } else if (o instanceof float[]) { float[] arr = (float[]) o; float[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH); return truncated("float", Arrays.toString(arr2), arr.length); //$NON-NLS-1$ } else if (o instanceof double[]) { double[] arr = (double[]) o; double[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH); return truncated("double", Arrays.toString(arr2), arr.length); //$NON-NLS-1$ } else if (o instanceof boolean[]) { boolean[] arr = (boolean[]) o; boolean[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH); return truncated("boolean", Arrays.toString(arr2), arr.length); //$NON-NLS-1$ } else if (o instanceof Object[]) { Object[] arr = (Object[]) o; Object[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH); return truncated("Object", Arrays.toString(arr2), arr.length); //$NON-NLS-1$ } else { return "Unknown big array " + o.getClass(); //$NON-NLS-1$ } } else { return o.getClass().getComponentType() + "[" + length + "] = " + ObjectUtils.toString(o); //$NON-NLS-1$ //$NON-NLS-2$ } } return null; } protected String truncated(String type, String string, int originalLength) { return type + "[" + RESOURCE_NAME_MAX_LENGTH + "/" + originalLength + "] = " + string; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } protected String getVariableName(ReadGraph graph, Variable r) { try { return r.getName(graph); } catch (Exception e) { return e.getMessage(); } } protected String getValue(ReadGraph graph, Variable base, Object o) throws DatabaseException { Class clazz = o.getClass(); if(o instanceof Connection) { Connection c = (Connection)o; TreeSet rvis = new TreeSet<>(); for(VariableConnectionPointDescriptor v : c.getConnectionPointDescriptors(graph, null)) { rvis.add(v.getRelativeRVI(graph, base)); } return "c " + rvis.toString(); //$NON-NLS-1$ } else if (clazz.isArray()) { if(int[].class == clazz) { return Arrays.toString((int[])o); } else if(float[].class == clazz) { return Arrays.toString((float[])o); } else if(double[].class == clazz) { return Arrays.toString((double[])o); } else if(long[].class == clazz) { return Arrays.toString((long[])o); } else if(byte[].class == clazz) { return Arrays.toString((byte[])o); } else if(boolean[].class == clazz) { return Arrays.toString((boolean[])o); } else if(char[].class == clazz) { return Arrays.toString((char[])o); } else { return Arrays.toString((Object[])o); } } return o.toString(); } protected String getValue(ReadGraph graph, Variable r) { try { Object value = r.getValue(graph); if(value instanceof Resource) return getResourceRef(graph, (Resource)value); else if (value instanceof Variable) return getVariableRef(graph, (Variable)value); else return value != null ? getValue(graph, r, value) : "null"; //$NON-NLS-1$ } catch (Throwable e) { try { LOGGER.error("getValue({})", r.getURI(graph), e); //$NON-NLS-1$ } catch (DatabaseException e1) { LOGGER.error("Failed to get URI for problematic value variable", e1); } return e.getMessage(); } } protected String getDatatype(ReadGraph graph, Variable r) { try { Datatype dt = r.getPossibleDatatype(graph); return dt != null ? dt.toSingleLineString() : "undefined"; //$NON-NLS-1$ } catch (Exception e) { return e.getMessage(); } } private String getResourceRef(ReadGraph graph, Resource r) throws DatabaseException { return getVariableRef(graph, graph.adapt(r, Variable.class)); } private String getVariableRef(ReadGraph graph, Variable r) throws DatabaseException { String ret = "" //$NON-NLS-1$ //$NON-NLS-2$ + getVariableName(graph, r) + ""; //$NON-NLS-1$ // if (graph.isInstanceOf(r, L0.Literal)) { // ret += " " // + "(edit value)" // + ""; // } return ret; } private String getLinkString(ReadGraph graph, Variable t) throws DatabaseException { try { String uri = t.getURI(graph); //return uri; String encoded = Base64.encode(uri.getBytes(utf8)); return encoded; } catch (Exception e) { LOGGER.error("Failed to construct link string for variable", e); //$NON-NLS-1$ return e.getMessage(); } } private void updateProperty(StringBuilder content, ReadGraph graph, Variable property) throws DatabaseException { // try { // System.out.println("update property " + property.getURI(graph)); // } catch (Exception e) { // e.printStackTrace(); // } content.append(""); //$NON-NLS-1$ content.append("").append(getVariableRef(graph, property)).append(""); //$NON-NLS-1$ //$NON-NLS-2$ content.append("").append(getValue(graph, property)).append(""); //$NON-NLS-1$ //$NON-NLS-2$ content.append("").append(getDatatype(graph, property)).append(""); //$NON-NLS-1$ //$NON-NLS-2$ content.append(""); //$NON-NLS-1$ } protected String getRVIString(ReadGraph graph, Variable var) throws DatabaseException { try { return var.getRVI(graph).toString(graph); } catch (Throwable e) { return "No RVI"; //$NON-NLS-1$ } } protected synchronized String calculateContent(final ReadGraph graph, String... uris) throws DatabaseException { L0 = Layer0.getInstance(graph); StringBuilder content = new StringBuilder(); // Generate HTML -page content.append("").append(getHead()).append("\n"); //$NON-NLS-1$ //$NON-NLS-2$ content.append("\n"); //$NON-NLS-1$ content.append("
\n"); //$NON-NLS-1$ for (String uri : uris) { //System.out.println("URI: " + uri); Variable var = Variables.getPossibleVariable(graph, uri); if (var == null) continue; String rviString = getRVIString(graph, var); Object node = null; if(var instanceof AbstractChildVariable) { VariableNode vn = ((AbstractChildVariable)var).node; if(vn != null) node = vn.node; } if(var instanceof AbstractPropertyVariable) { VariableNode vn = ((AbstractPropertyVariable)var).node; if(vn != null) node = vn.node; } // Begin #top DIV content.append("
\n"); //$NON-NLS-1$ content.append("\n"); //$NON-NLS-1$ content.append("\n"); //$NON-NLS-1$ //$NON-NLS-2$ content.append("\n"); //$NON-NLS-1$ //$NON-NLS-2$ content.append("\n"); //$NON-NLS-1$ //$NON-NLS-2$ content.append("\n"); //$NON-NLS-1$ //$NON-NLS-2$ content.append("
URI").append(uri).append("
RVI").append(rviString).append("
Class").append(var.getClass().getCanonicalName()).append("
Solver node").append(node).append("
\n"); //$NON-NLS-1$ content.append("
\n"); //$NON-NLS-1$ // Close #top DIV // Content TreeMap map = new TreeMap(); try { for(Variable child : var.getChildren(graph)) { String name = getVariableName(graph, child); map.put(name, child); } } catch (DatabaseException e) { // This may happen if the Variable implementation is broken ErrorLogger.defaultLogError("Broken variable child retrieval implementation or serious modelling error encountered. See exception for details.", e); //$NON-NLS-1$ } TreeMap map2 = new TreeMap(); try { for(Variable child : var.getProperties(graph)) { String name = getVariableName(graph, child); map2.put(name, child); } } catch (DatabaseException e) { // This may happen if the Variable implementation is broken ErrorLogger.defaultLogError("Broken variable property retrieval implementation or serious modelling error encountered. See exception for details.", e); //$NON-NLS-1$ } content.append("\n
\n"); //$NON-NLS-1$ content.append("\n"); //$NON-NLS-1$ content.append(""); //$NON-NLS-1$ for (Variable child : map.values()) { content.append(""); //$NON-NLS-1$ //$NON-NLS-2$ } content.append(""); //$NON-NLS-1$ for (Variable property : map2.values()) { updateProperty(content, graph, property); } // Close #data content.append("\n\n"); //$NON-NLS-1$ } // Close #mainContent content.append("\n"); //$NON-NLS-1$ content.append("\n"); //$NON-NLS-1$ // Update content return content.toString(); } private String getHead() { String result = ""; //$NON-NLS-1$ if (cssPath != null) { result = ""; //$NON-NLS-1$ //$NON-NLS-2$ } return result; } }
Child
").append(getVariableRef(graph, child)).append("
PropertyValueDatatype