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