1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.debug.ui;
15 import java.io.FileNotFoundException;
16 import java.io.IOException;
17 import java.lang.reflect.Array;
19 import java.nio.charset.Charset;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.LinkedList;
23 import java.util.TreeMap;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.atomic.AtomicReference;
27 import org.eclipse.core.runtime.Assert;
28 import org.eclipse.core.runtime.FileLocator;
29 import org.eclipse.core.runtime.IPath;
30 import org.eclipse.core.runtime.Path;
31 import org.eclipse.jface.layout.GridDataFactory;
32 import org.eclipse.jface.resource.ColorDescriptor;
33 import org.eclipse.jface.resource.JFaceResources;
34 import org.eclipse.jface.resource.LocalResourceManager;
35 import org.eclipse.swt.SWT;
36 import org.eclipse.swt.SWTError;
37 import org.eclipse.swt.browser.Browser;
38 import org.eclipse.swt.browser.LocationAdapter;
39 import org.eclipse.swt.browser.LocationEvent;
40 import org.eclipse.swt.dnd.DND;
41 import org.eclipse.swt.dnd.DropTarget;
42 import org.eclipse.swt.dnd.DropTargetAdapter;
43 import org.eclipse.swt.dnd.DropTargetEvent;
44 import org.eclipse.swt.dnd.TextTransfer;
45 import org.eclipse.swt.dnd.Transfer;
46 import org.eclipse.swt.events.DisposeEvent;
47 import org.eclipse.swt.events.DisposeListener;
48 import org.eclipse.swt.events.KeyAdapter;
49 import org.eclipse.swt.events.KeyEvent;
50 import org.eclipse.swt.events.SelectionEvent;
51 import org.eclipse.swt.events.SelectionListener;
52 import org.eclipse.swt.graphics.Color;
53 import org.eclipse.swt.graphics.RGB;
54 import org.eclipse.swt.layout.GridData;
55 import org.eclipse.swt.layout.GridLayout;
56 import org.eclipse.swt.widgets.Button;
57 import org.eclipse.swt.widgets.Composite;
58 import org.eclipse.swt.widgets.Label;
59 import org.eclipse.swt.widgets.Text;
60 import org.simantics.databoard.type.Datatype;
61 import org.simantics.databoard.util.ObjectUtils;
62 import org.simantics.db.ReadGraph;
63 import org.simantics.db.Resource;
64 import org.simantics.db.Session;
65 import org.simantics.db.common.ResourceArray;
66 import org.simantics.db.common.procedure.adapter.DisposableListener;
67 import org.simantics.db.common.request.UnaryRead;
68 import org.simantics.db.common.utils.Logger;
69 import org.simantics.db.exception.DatabaseException;
70 import org.simantics.db.layer0.SelectionHints;
71 import org.simantics.db.layer0.request.PossibleURI;
72 import org.simantics.db.layer0.request.ResourceURIToVariable;
73 import org.simantics.db.layer0.request.VariableURI;
74 import org.simantics.db.layer0.variable.AbstractChildVariable;
75 import org.simantics.db.layer0.variable.AbstractPropertyVariable;
76 import org.simantics.db.layer0.variable.Variable;
77 import org.simantics.db.layer0.variable.VariableNode;
78 import org.simantics.db.layer0.variable.Variables;
79 import org.simantics.db.service.SerialisationSupport;
80 import org.simantics.debug.ui.internal.Activator;
81 import org.simantics.layer0.Layer0;
82 import org.simantics.structural2.variables.Connection;
83 import org.simantics.structural2.variables.VariableConnectionPointDescriptor;
84 import org.simantics.ui.dnd.LocalObjectTransfer;
85 import org.simantics.ui.dnd.ResourceReferenceTransfer;
86 import org.simantics.ui.dnd.ResourceTransferUtils;
87 import org.simantics.ui.utils.ResourceAdaptionUtils;
88 import org.simantics.utils.FileUtils;
89 import org.simantics.utils.bytes.Base64;
90 import org.simantics.utils.ui.ErrorLogger;
91 import org.simantics.utils.ui.ISelectionUtils;
92 import org.simantics.utils.ui.PathUtils;
96 * @author Antti Villberg
97 * @author Tuukka Lehtonen
99 public class VariableDebugger extends Composite {
101 public interface HistoryListener {
102 void historyChanged();
105 private final static String DEFAULT_DEBUGGER_CSS_FILE = "debugger.css";
106 private final static String DEFAULT_DEBUGGER_CSS_PATH = "css/" + DEFAULT_DEBUGGER_CSS_FILE;
108 private static int RESOURCE_NAME_MAX_LENGTH = 1000;
110 private final Charset utf8 = Charset.forName("UTF-8");
112 private final LocalResourceManager resourceManager;
114 private String cssPath;
116 private Text updateTriggerCounter;
117 private Browser browser;
118 private final ColorDescriptor green = ColorDescriptor.createFrom(new RGB(0x57, 0xbc, 0x95));
120 private final LinkedList<String> backHistory = new LinkedList<String>();
121 private final LinkedList<String> forwardHistory = new LinkedList<String>();
122 private String currentElement = null;
125 * The Session used to access the graph. Received from outside of this
126 * class and therefore it is not disposed here, just used.
128 private final Session session;
130 private final CopyOnWriteArrayList<HistoryListener> historyListeners = new CopyOnWriteArrayList<HistoryListener>();
134 protected boolean disposed;
136 class PageContentListener extends DisposableListener<String> {
139 AtomicReference<String> lastResult = new AtomicReference<String>();
141 public void execute(final String content) {
143 //System.out.println("LISTENER TRIGGERED: " + triggerCounter);
144 //System.out.println("LISTENER:\n" + content);
145 if (lastResult.getAndSet(content) == null) {
147 getDisplay().asyncExec(new Runnable() {
150 String content = lastResult.getAndSet(null);
155 //System.out.println("UPDATE " + updateCount);
157 if (!browser.isDisposed())
158 browser.setText(content);
159 if (!updateTriggerCounter.isDisposed())
160 updateTriggerCounter.setText(updateCount + "/" + triggerCounter);
168 public void exception(Throwable t) {
169 Logger.defaultLogError(t);
173 private PageContentListener pageContentListener;
179 * @param resource the initial resource to debug or <code>null</code> for
180 * initially blank UI.
182 public VariableDebugger(Composite parent, int style, final Session session, String initialURI) {
183 super(parent, style);
184 Assert.isNotNull(session, "session is null");
185 this.session = session;
186 this.currentElement = initialURI;
187 this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), parent);
191 addDisposeListener(new DisposeListener() {
193 public void widgetDisposed(DisposeEvent e) {
195 PageContentListener l = pageContentListener;
202 public void defaultInitializeUI() {
203 setLayout(new GridLayout(4, false));
204 setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
206 createDropLabel(this);
207 createResourceText(this);
208 createUpdateTriggerCounter(this);
209 Browser browser = createBrowser(this);
210 GridDataFactory.fillDefaults().span(4, 1).grab(true, true).applyTo(browser);
213 protected void initializeCSS() {
214 // Extract default css to a temporary location if necessary.
216 IPath absolutePath = PathUtils.getAbsolutePath(Activator.PLUGIN_ID, DEFAULT_DEBUGGER_CSS_PATH);
217 if (absolutePath != null) {
218 cssPath = absolutePath.toFile().toURI().toString();
220 File tempDir = FileUtils.getOrCreateTemporaryDirectory(false);
221 File css = new File(tempDir, DEFAULT_DEBUGGER_CSS_FILE);
223 URL url = FileLocator.find(Activator.getDefault().getBundle(), new Path(DEFAULT_DEBUGGER_CSS_PATH), null);
225 throw new FileNotFoundException("Could not find '" + DEFAULT_DEBUGGER_CSS_PATH + "' in bundle '" + Activator.PLUGIN_ID + "'");
226 cssPath = FileUtils.copyResource(url, css, true).toURI().toString();
228 cssPath = css.toURI().toString();
231 } catch (IOException e) {
233 // CSS extraction failed, let's just live without it then.
234 ErrorLogger.defaultLogWarning(e);
238 public Label createDropLabel(Composite parent) {
239 final Label label = new Label(parent, SWT.BORDER | SWT.FLAT);
240 label.setAlignment(SWT.CENTER);
241 label.setText(" Drag a resource or a variable here to examine it in this debugger! ");
242 label.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY));
243 GridData data = new GridData(SWT.LEFT, SWT.FILL, false, false);
244 label.setLayoutData(data);
246 // Add resource id drop support to the drop-area.
247 DropTarget dropTarget = new DropTarget(label, DND.DROP_LINK | DND.DROP_COPY);
248 dropTarget.setTransfer(new Transfer[] { TextTransfer.getInstance(), ResourceReferenceTransfer.getInstance(), LocalObjectTransfer.getTransfer() });
249 dropTarget.addDropListener(new DropTargetAdapter() {
251 public void dragEnter(DropTargetEvent event) {
252 event.detail = DND.DROP_LINK;
253 label.setBackground((Color) resourceManager.get(green));
257 public void dragLeave(DropTargetEvent event) {
258 label.setBackground(null);
262 public void drop(DropTargetEvent event) {
263 label.setBackground(null);
265 String uri = parseUri(event);
267 event.detail = DND.DROP_NONE;
271 } catch (DatabaseException e) {
272 Logger.defaultLogError(e);
276 private String parseUri(DropTargetEvent event) throws DatabaseException {
277 Variable v = parseVariable(event);
278 String uri = v != null ? session.sync(new VariableURI(v)) : null;
280 Resource r = parseResource(event);
281 uri = r != null ? session.sync(new PossibleURI(r)) : null;
286 private Variable parseVariable(DropTargetEvent event) {
287 return ISelectionUtils.getSinglePossibleKey(event.data, SelectionHints.KEY_MAIN, Variable.class);
290 private Resource parseResource(DropTargetEvent event) throws DatabaseException {
291 ResourceArray[] ra = null;
292 if (event.data instanceof String) {
294 SerialisationSupport support = session.getService(SerialisationSupport.class);
295 ra = ResourceTransferUtils.readStringTransferable(support, (String) event.data).toResourceArrayArray();
296 } catch (IllegalArgumentException e) {
298 } catch (DatabaseException e) {
302 ra = ResourceAdaptionUtils.toResourceArrays(event.data);
304 if (ra != null && ra.length > 0)
305 return ra[0].resources[ra[0].resources.length - 1];
313 public void createResourceText(Composite parent) {
314 final Text text = new Text(parent, SWT.BORDER);
315 GridData data = new GridData(SWT.FILL, SWT.FILL, true, false);
316 text.setLayoutData(data);
318 Button button = new Button(parent, SWT.NONE);
319 button.setText("Lookup");
320 GridData data2 = new GridData(SWT.FILL, SWT.FILL, false, false);
321 button.setLayoutData(data2);
323 button.addSelectionListener(new SelectionListener() {
326 public void widgetDefaultSelected(SelectionEvent e) {
331 public void widgetSelected(SelectionEvent e) {
334 String uri = text.getText();
335 // Make sure that URI is resolvable to Variable
336 session.sync(new ResourceURIToVariable(uri));
338 } catch (DatabaseException e1) {
339 Logger.defaultLogError(e1);
347 protected Text createUpdateTriggerCounter(Composite parent) {
348 Text label = new Text(parent, SWT.BORDER | SWT.FLAT);
349 label.setEditable(false);
350 label.setToolTipText("Amount of Screen/Listener Updates Received for Shown Variable");
351 GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
352 .grab(false, false).hint(32, SWT.DEFAULT).applyTo(label);
353 updateTriggerCounter = label;
357 public Browser createBrowser(Composite parent) {
359 browser = new Browser(parent, SWT.MOZILLA);
360 } catch (SWTError e) {
361 //System.out.println("Could not instantiate Browser: " + e.getMessage());
362 browser = new Browser(parent, SWT.NONE);
364 browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
366 // Left/right arrows for back/forward
367 browser.addKeyListener(new KeyAdapter() {
369 public void keyReleased(KeyEvent e) {
370 // System.out.println("key, char: " + e.keyCode + ", " + (int) e.character + " (" + e.character + ")");
371 // if (e.keyCode == SWT.BS) {
374 if ((e.stateMask & SWT.ALT) != 0) {
375 if (e.keyCode == SWT.ARROW_RIGHT)
377 if (e.keyCode == SWT.ARROW_LEFT)
383 // Add listener for debugging functionality
384 browser.addLocationListener(new LocationAdapter() {
386 public void changing(LocationEvent event) {
387 String location = event.location;
388 if (location.startsWith("simantics:browser"))
389 location = "about:" + location.substring(17);
390 //System.out.println("changing: location=" + location);
392 // Do not follow links that are meant as actions that are
395 if ("about:blank".equals(location)) {
396 // Just changing to the same old blank url is ok since it
397 // allows the browser to refresh itself.
401 if (location.startsWith("about:-link")) {
402 String target = location.replace("about:-link", "");
404 byte[] bytes = Base64.decode(target);
405 String url = new String(bytes, utf8);
406 if (url.equals(currentElement)) {
411 } catch (IOException e) {
412 ErrorLogger.defaultLogError(e);
414 } else if (location.startsWith("about:-remove")) {
415 } else if (location.startsWith("about:-edit-value")) {
420 // Schedule a request that updates the browser content.
426 public void refreshBrowser() {
427 if (currentElement == null)
430 // Schedule a request that updates the browser content.
431 if (pageContentListener != null)
432 pageContentListener.dispose();
433 pageContentListener = new PageContentListener();
434 session.asyncRequest(new UnaryRead<String, String>(currentElement) {
436 public String perform(ReadGraph graph) throws DatabaseException {
437 String content = calculateContent(graph, parameter);
438 //System.out.println("HTML: " + content);
441 }, pageContentListener);
445 public String getDebuggerLocation() {
446 return currentElement;
449 public void changeLocation(String url) {
450 if (currentElement != null) {
451 backHistory.addLast(currentElement);
453 currentElement = url;
454 forwardHistory.clear();
457 fireHistoryChanged();
460 public void addHistoryListener(HistoryListener l) {
461 historyListeners.add(l);
464 public void removeHistoryListener(HistoryListener l) {
465 historyListeners.remove(l);
468 private void fireHistoryChanged() {
469 for (HistoryListener l : historyListeners)
473 public boolean hasBackHistory() {
474 return backHistory.isEmpty();
477 public boolean hasForwardHistory() {
478 return forwardHistory.isEmpty();
482 if (backHistory.isEmpty())
485 forwardHistory.addFirst(currentElement);
486 currentElement = backHistory.removeLast();
489 fireHistoryChanged();
492 public void forward() {
493 if (forwardHistory.isEmpty())
496 backHistory.addLast(currentElement);
497 currentElement = forwardHistory.removeFirst();
500 fireHistoryChanged();
503 protected String toName(Object o) {
504 Class<?> clazz = o.getClass();
505 if (clazz.isArray()) {
506 int length = Array.getLength(o);
507 if (length > RESOURCE_NAME_MAX_LENGTH) {
508 if (o instanceof byte[]) {
509 byte[] arr = (byte[]) o;
510 byte[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
511 return truncated("byte", Arrays.toString(arr2), arr.length);
512 } else if (o instanceof int[]) {
513 int[] arr = (int[]) o;
514 int[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
515 return truncated("int", Arrays.toString(arr2), arr.length);
516 } else if (o instanceof long[]) {
517 long[] arr = (long[]) o;
518 long[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
519 return truncated("long", Arrays.toString(arr2), arr.length);
520 } else if (o instanceof float[]) {
521 float[] arr = (float[]) o;
522 float[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
523 return truncated("float", Arrays.toString(arr2), arr.length);
524 } else if (o instanceof double[]) {
525 double[] arr = (double[]) o;
526 double[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
527 return truncated("double", Arrays.toString(arr2), arr.length);
528 } else if (o instanceof boolean[]) {
529 boolean[] arr = (boolean[]) o;
530 boolean[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
531 return truncated("boolean", Arrays.toString(arr2), arr.length);
532 } else if (o instanceof Object[]) {
533 Object[] arr = (Object[]) o;
534 Object[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
535 return truncated("Object", Arrays.toString(arr2), arr.length);
537 return "Unknown big array " + o.getClass();
540 return o.getClass().getComponentType() + "[" + length + "] = " + ObjectUtils.toString(o);
546 protected String truncated(String type, String string, int originalLength) {
547 return type + "[" + RESOURCE_NAME_MAX_LENGTH + "/" + originalLength + "] = " + string;
550 protected String getVariableName(ReadGraph graph, Variable r) {
552 return r.getName(graph);
553 } catch (Exception e) {
554 return e.getMessage();
558 protected String getValue(ReadGraph graph, Variable base, Object o) throws DatabaseException {
559 Class<?> clazz = o.getClass();
560 if(o instanceof Connection) {
561 Connection c = (Connection)o;
562 ArrayList<String> result = new ArrayList<String>();
563 for(VariableConnectionPointDescriptor v : c.getConnectionPointDescriptors(graph, null)) {
564 result.add(v.getRelativeRVI(graph, base));
566 return "c " + result.toString();
567 } else if (clazz.isArray()) {
568 if(int[].class == clazz) {
569 return Arrays.toString((int[])o);
570 } else if(float[].class == clazz) {
571 return Arrays.toString((float[])o);
572 } else if(double[].class == clazz) {
573 return Arrays.toString((double[])o);
574 } else if(long[].class == clazz) {
575 return Arrays.toString((long[])o);
576 } else if(byte[].class == clazz) {
577 return Arrays.toString((byte[])o);
578 } else if(boolean[].class == clazz) {
579 return Arrays.toString((boolean[])o);
580 } else if(char[].class == clazz) {
581 return Arrays.toString((char[])o);
583 return Arrays.toString((Object[])o);
589 protected String getValue(ReadGraph graph, Variable r) {
591 Object value = r.getValue(graph);
592 if(value instanceof Resource) return getResourceRef(graph, (Resource)value);
593 else if (value instanceof Variable) return getVariableRef(graph, (Variable)value);
594 else return value != null ? getValue(graph, r, value) : "null";
595 } catch (Throwable e) {
597 Logger.defaultLogError("getValue " + r.getURI(graph), e);
598 } catch (DatabaseException e1) {
599 Logger.defaultLogError(e1);
601 return e.getMessage();
605 protected String getDatatype(ReadGraph graph, Variable r) {
607 Datatype dt = r.getPossibleDatatype(graph);
608 return dt != null ? dt.toSingleLineString() : "undefined";
609 } catch (Exception e) {
610 return e.getMessage();
614 private String getResourceRef(ReadGraph graph, Resource r) throws DatabaseException {
615 return getVariableRef(graph, graph.adapt(r, Variable.class));
618 private String getVariableRef(ReadGraph graph, Variable r) throws DatabaseException {
619 String ret = "<a href=\"simantics:browser-link" + getLinkString(graph, r) + "\">"
620 + getVariableName(graph, r)
622 // if (graph.isInstanceOf(r, L0.Literal)) {
623 // ret += " <a class=\"edit-link\" href=\"simantics:browser-edit-value" + getLinkString(r) + "\">"
630 private String getLinkString(ReadGraph graph, Variable t) throws DatabaseException {
632 String uri = t.getURI(graph);
634 String encoded = Base64.encode(uri.getBytes(utf8));
636 } catch (Exception e) {
637 Logger.defaultLogError(e);
638 return e.getMessage();
642 private void updateProperty(StringBuilder content, ReadGraph graph, Variable property) throws DatabaseException {
644 // System.out.println("update property " + property.getURI(graph));
645 // } catch (Exception e) {
646 // e.printStackTrace();
648 content.append("<tr>");
649 content.append("<td>").append(getVariableRef(graph, property)).append("</td>");
650 content.append("<td>").append(getValue(graph, property)).append("</td>");
651 content.append("<td>").append(getDatatype(graph, property)).append("</td>");
652 content.append("</tr>");
655 protected String getRVIString(ReadGraph graph, Variable var) throws DatabaseException {
658 return var.getRVI(graph).toString(graph);
659 } catch (Throwable e) {
665 protected synchronized String calculateContent(final ReadGraph graph, String... uris) throws DatabaseException {
667 L0 = Layer0.getInstance(graph);
669 StringBuilder content = new StringBuilder();
671 // Generate HTML -page
672 content.append("<html><head>").append(getHead()).append("</head>\n");
673 content.append("<body>\n");
674 content.append("<div id=\"mainContent\">\n");
675 for (String uri : uris) {
676 //System.out.println("URI: " + uri);
677 Variable var = Variables.getPossibleVariable(graph, uri);
681 String rviString = getRVIString(graph, var);
683 if(var instanceof AbstractChildVariable) {
684 VariableNode vn = ((AbstractChildVariable)var).node;
685 if(vn != null) node = vn.node;
687 if(var instanceof AbstractPropertyVariable) {
688 VariableNode vn = ((AbstractPropertyVariable)var).node;
689 if(vn != null) node = vn.node;
693 content.append("<div id=\"top\">\n");
694 content.append("<table class=\"top\">\n");
695 content.append("<tr><td class=\"top_key\">URI</td><td class=\"top_value\"><span id=\"uri\">").append(uri).append("</span></td></tr>\n");
696 content.append("<tr><td class=\"top_key\">RVI</td><td class=\"top_value\"><span id=\"uri\">").append(rviString).append("</span></td></tr>\n");
697 content.append("<tr><td class=\"top_key\">Class</td><td class=\"top_value\"><span id=\"class\">").append(var.getClass().getCanonicalName()).append("</span></td></tr>\n");
698 content.append("<tr><td class=\"top_key\">Solver node</td><td class=\"top_value\"><span id=\"class\">").append(node).append("</span></td></tr>\n");
699 content.append("</table>\n");
700 content.append("</div>\n");
704 TreeMap<String, Variable> map = new TreeMap<String, Variable>();
706 for(Variable child : var.getChildren(graph)) {
707 String name = getVariableName(graph, child);
708 map.put(name, child);
710 } catch (DatabaseException e) {
711 // This may happen if the Variable implementation is broken
712 ErrorLogger.defaultLogError("Broken variable child retrieval implementation or serious modelling error encountered. See exception for details.", e);
715 TreeMap<String, Variable> map2 = new TreeMap<String, Variable>();
717 for(Variable child : var.getProperties(graph)) {
718 String name = getVariableName(graph, child);
719 map2.put(name, child);
721 } catch (DatabaseException e) {
722 // This may happen if the Variable implementation is broken
723 ErrorLogger.defaultLogError("Broken variable property retrieval implementation or serious modelling error encountered. See exception for details.", e);
726 content.append("\n<div id=\"data\">\n");
727 content.append("<table>\n");
729 content.append("<tr><th>Child</th></tr>");
730 for (Variable child : map.values()) {
731 content.append("<tr><td>").append(getVariableRef(graph, child)).append("</td></tr>");
734 content.append("<tr><th>Property</th><th>Value</th><th>Datatype</th></tr>");
735 for (Variable property : map2.values()) {
736 updateProperty(content, graph, property);
739 content.append("</div>\n\n");
742 // Close #mainContent
743 content.append("</div>\n");
744 content.append("</body></html>\n");
747 return content.toString();
750 private String getHead() {
752 if (cssPath != null) {
753 result = "<link href=\"" + cssPath + "\" rel=\"stylesheet\" type=\"text/css\">";