Stop using SWT.MOZILLA
[simantics/platform.git] / bundles / org.simantics.debug.ui / src / org / simantics / debug / ui / GraphDebugger.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.debug.ui;
13
14 import java.io.File;
15 import java.io.FileNotFoundException;
16 import java.io.IOException;
17 import java.lang.reflect.Array;
18 import java.net.URI;
19 import java.net.URISyntaxException;
20 import java.net.URL;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Comparator;
25 import java.util.HashMap;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.UUID;
30 import java.util.concurrent.CopyOnWriteArrayList;
31
32 import org.eclipse.core.runtime.Assert;
33 import org.eclipse.core.runtime.FileLocator;
34 import org.eclipse.core.runtime.IPath;
35 import org.eclipse.core.runtime.Path;
36 import org.eclipse.jface.action.IStatusLineManager;
37 import org.eclipse.jface.dialogs.Dialog;
38 import org.eclipse.jface.dialogs.IDialogConstants;
39 import org.eclipse.jface.dialogs.IDialogSettings;
40 import org.eclipse.jface.dialogs.IInputValidator;
41 import org.eclipse.jface.dialogs.InputDialog;
42 import org.eclipse.jface.dialogs.MessageDialog;
43 import org.eclipse.jface.layout.GridDataFactory;
44 import org.eclipse.jface.resource.ColorDescriptor;
45 import org.eclipse.jface.resource.JFaceResources;
46 import org.eclipse.jface.resource.LocalResourceManager;
47 import org.eclipse.osgi.util.NLS;
48 import org.eclipse.swt.SWT;
49 import org.eclipse.swt.SWTError;
50 import org.eclipse.swt.browser.Browser;
51 import org.eclipse.swt.browser.LocationAdapter;
52 import org.eclipse.swt.browser.LocationEvent;
53 import org.eclipse.swt.dnd.DND;
54 import org.eclipse.swt.dnd.DropTarget;
55 import org.eclipse.swt.dnd.DropTargetAdapter;
56 import org.eclipse.swt.dnd.DropTargetEvent;
57 import org.eclipse.swt.dnd.TextTransfer;
58 import org.eclipse.swt.dnd.Transfer;
59 import org.eclipse.swt.events.DisposeEvent;
60 import org.eclipse.swt.events.DisposeListener;
61 import org.eclipse.swt.events.FocusEvent;
62 import org.eclipse.swt.events.FocusListener;
63 import org.eclipse.swt.events.KeyAdapter;
64 import org.eclipse.swt.events.KeyEvent;
65 import org.eclipse.swt.events.ModifyEvent;
66 import org.eclipse.swt.events.ModifyListener;
67 import org.eclipse.swt.events.SelectionEvent;
68 import org.eclipse.swt.events.SelectionListener;
69 import org.eclipse.swt.graphics.Color;
70 import org.eclipse.swt.graphics.Point;
71 import org.eclipse.swt.graphics.RGB;
72 import org.eclipse.swt.layout.GridData;
73 import org.eclipse.swt.layout.GridLayout;
74 import org.eclipse.swt.widgets.Button;
75 import org.eclipse.swt.widgets.Composite;
76 import org.eclipse.swt.widgets.Label;
77 import org.eclipse.swt.widgets.Text;
78 import org.eclipse.ui.IWorkbenchSite;
79 import org.simantics.databoard.Bindings;
80 import org.simantics.databoard.Databoard;
81 import org.simantics.databoard.binding.Binding;
82 import org.simantics.databoard.binding.error.BindingException;
83 import org.simantics.databoard.serialization.Serializer;
84 import org.simantics.databoard.type.ArrayType;
85 import org.simantics.databoard.type.Datatype;
86 import org.simantics.databoard.type.StringType;
87 import org.simantics.databoard.util.ObjectUtils;
88 import org.simantics.db.AsyncRequestProcessor;
89 import org.simantics.db.ReadGraph;
90 import org.simantics.db.Resource;
91 import org.simantics.db.Session;
92 import org.simantics.db.Statement;
93 import org.simantics.db.VirtualGraph;
94 import org.simantics.db.WriteGraph;
95 import org.simantics.db.common.ResourceArray;
96 import org.simantics.db.common.procedure.adapter.ProcedureAdapter;
97 import org.simantics.db.common.request.Queries;
98 import org.simantics.db.common.request.ReadRequest;
99 import org.simantics.db.common.request.WriteRequest;
100 import org.simantics.db.common.uri.ResourceToPossibleURI;
101 import org.simantics.db.common.utils.ListUtils;
102 import org.simantics.db.common.utils.NameUtils;
103 import org.simantics.db.common.utils.OrderedSetUtils;
104 import org.simantics.db.event.ChangeEvent;
105 import org.simantics.db.event.ChangeListener;
106 import org.simantics.db.exception.AdaptionException;
107 import org.simantics.db.exception.DatabaseException;
108 import org.simantics.db.exception.ResourceNotFoundException;
109 import org.simantics.db.exception.ValidationException;
110 import org.simantics.db.layer0.adapter.StringModifier;
111 import org.simantics.db.layer0.variable.RVI;
112 import org.simantics.db.layer0.variable.Variable;
113 import org.simantics.db.layer0.variable.Variables;
114 import org.simantics.db.service.ClusteringSupport;
115 import org.simantics.db.service.GraphChangeListenerSupport;
116 import org.simantics.db.service.SerialisationSupport;
117 import org.simantics.db.service.VirtualGraphSupport;
118 import org.simantics.db.service.XSupport;
119 import org.simantics.debug.ui.internal.Activator;
120 import org.simantics.debug.ui.internal.DebugUtils;
121 import org.simantics.debug.ui.internal.HashMultiMap;
122 import org.simantics.layer0.Layer0;
123 import org.simantics.ui.dnd.LocalObjectTransfer;
124 import org.simantics.ui.dnd.ResourceReferenceTransfer;
125 import org.simantics.ui.dnd.ResourceTransferUtils;
126 import org.simantics.ui.utils.ResourceAdaptionUtils;
127 import org.simantics.utils.Container;
128 import org.simantics.utils.FileUtils;
129 import org.simantics.utils.datastructures.BijectionMap;
130 import org.simantics.utils.strings.AlphanumComparator;
131 import org.simantics.utils.ui.ErrorLogger;
132 import org.simantics.utils.ui.PathUtils;
133 import org.simantics.utils.ui.workbench.WorkbenchUtils;
134
135
136 public class GraphDebugger extends Composite {
137
138     public interface HistoryListener {
139         void historyChanged();
140     }
141
142     private static final String                         STATEMENT_PART_SEPARATOR  = ","; //$NON-NLS-1$
143     private final static String                         DEFAULT_DEBUGGER_CSS_FILE = "debugger.css"; //$NON-NLS-1$
144     private final static String                         DEFAULT_DEBUGGER_CSS_PATH = "css/" + DEFAULT_DEBUGGER_CSS_FILE; //$NON-NLS-1$
145
146     private static int                                  RESOURCE_NAME_MAX_LENGTH  = 1000;
147     private static int                                  RESOURCE_VALUE_MAX_SIZE = 16384;
148
149     private final LocalResourceManager                  resourceManager;
150
151     private String                                      cssPath;
152
153     private Browser                                     browser;
154     private final ColorDescriptor                       green = ColorDescriptor.createFrom(new RGB(0x57, 0xbc, 0x95));
155
156     private final boolean                               displayClusters           = true;
157
158     private final BijectionMap<String, Resource>        links                     = new BijectionMap<String, Resource>();
159     private final LinkedList<Resource>                  backHistory               = new LinkedList<Resource>();
160     private final LinkedList<Resource>                  forwardHistory            = new LinkedList<Resource>();
161     private Resource                                    currentElement            = null;
162
163     /**
164      * The Session used to access the graph. Received from outside of this
165      * class and therefore it is not disposed here, just used.
166      */
167     private final Session                               session;
168
169     private final CopyOnWriteArrayList<HistoryListener> historyListeners          = new CopyOnWriteArrayList<HistoryListener>();
170
171     private final AsyncRequestProcessor                 updater;
172
173     protected Layer0                                    L0;
174
175     protected IWorkbenchSite                            site;
176
177     private final ChangeListener changeListener = new ChangeListener() {
178         @Override
179         public void graphChanged(ChangeEvent e) {
180             // This makes sure that the transaction for updating this
181             // GraphDebugger get executed in a serialized fashion.
182             updater.asyncRequest(new ReadRequest() {
183
184                 @Override
185                 public void run(ReadGraph graph) throws DatabaseException {
186                     updateContent(graph, currentElement);
187                 }
188
189             });
190
191         }
192     };
193
194     /**
195      * @param parent
196      * @param style
197      * @param session
198      * @param resource the initial resource to debug or <code>null</code> for
199      *        initially blank UI.
200      * @param site the workbench site that contains this debugger, for workbench
201      *        service access
202      */
203     public GraphDebugger(Composite parent, int style, final Session session, Resource resource, IWorkbenchSite site) {
204         this(parent, style, session, resource);
205         this.site = site;
206     }
207
208     /**
209      * @param parent
210      * @param style
211      * @param session
212      * @param resource the initial resource to debug or <code>null</code> for
213      *        initially blank UI.
214      */
215     public GraphDebugger(Composite parent, int style, final Session session, Resource resource) {
216         super(parent, style);
217         Assert.isNotNull(session, "session is null"); //$NON-NLS-1$
218         this.session = session;
219         this.currentElement = resource;
220         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), parent);
221
222         updater = session;//.getService(MergingGraphRequestProcessor.class);
223
224         initializeCSS();
225
226         addDisposeListener(new DisposeListener() {
227             @Override
228             public void widgetDisposed(DisposeEvent e) {
229                 GraphChangeListenerSupport support = session.getService(GraphChangeListenerSupport.class);
230                 support.removeListener(changeListener);
231             }
232         });
233     }
234
235     /**
236      * When given to setStatus, indicates that the message shouldn't be touched
237      * since <code>null</code> has a different meaning.
238      */
239     private static final String DONT_TOUCH = "DONT_TOUCH"; //$NON-NLS-1$
240
241     protected void setStatus(String message, String error) {
242         IStatusLineManager status = WorkbenchUtils.getStatusLine(site);
243         if (status != null) {
244             if (message != DONT_TOUCH)
245                 status.setMessage(message);
246             if (error != DONT_TOUCH)
247                 status.setErrorMessage(error);
248         }
249     }
250
251     public void defaultInitializeUI() {
252         setLayout(new GridLayout(2, false));
253         setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
254
255         createResourceText(this);
256         createDropLabel(this);
257         createBrowser(this);
258
259     }
260
261     protected void initializeCSS() {
262         // Extract default css to a temporary location if necessary.
263         try {
264             IPath absolutePath = PathUtils.getAbsolutePath(Activator.PLUGIN_ID, DEFAULT_DEBUGGER_CSS_PATH);
265             if (absolutePath != null) {
266                 cssPath = absolutePath.toFile().toURI().toString();
267             } else {
268                 File tempDir = FileUtils.getOrCreateTemporaryDirectory(false);
269                 File css = new File(tempDir, DEFAULT_DEBUGGER_CSS_FILE);
270                 if (!css.exists()) {
271                     URL url = FileLocator.find(Activator.getDefault().getBundle(), new Path(DEFAULT_DEBUGGER_CSS_PATH), null);
272                     if (url == null)
273                         throw new FileNotFoundException("Could not find '" + DEFAULT_DEBUGGER_CSS_PATH + "' in bundle '" + Activator.PLUGIN_ID + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
274                     cssPath = FileUtils.copyResource(url, css, true).toURI().toString();
275                 } else {
276                     cssPath = css.toURI().toString();
277                 }
278             }
279         } catch (IOException e) {
280             // CSS extraction failed, let's just live without it then.
281             ErrorLogger.defaultLogWarning(e);
282         }
283     }
284
285     private static final String PROMPT_TEXT = Messages.GraphDebugger_EnterResourceIDorURI;
286
287     public void createResourceText(final Composite parent) {
288         final Text text = new Text(parent, SWT.BORDER);
289         text.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY));
290         text.setText(PROMPT_TEXT);
291         GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(text);
292         
293         text.addFocusListener(new FocusListener() {    
294             @Override
295             public void focusLost(FocusEvent e) {
296                 if (text.getText().trim().equals("")) { //$NON-NLS-1$
297                     text.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY));
298                     text.setText(PROMPT_TEXT);
299                 }
300             }
301             @Override
302             public void focusGained(FocusEvent e) {
303                 if (text.getText().trim().equals(PROMPT_TEXT)) {
304                     text.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_BLACK));
305                     text.setText(""); //$NON-NLS-1$
306                 }
307                 text.selectAll();
308             }
309         });
310         text.addKeyListener(new KeyAdapter() {
311             @Override
312             public void keyPressed(KeyEvent e) {
313                 if (e.keyCode == SWT.CR) {
314                     String input = text.getText();
315                     setLookupInput(input);
316                 }
317             }
318         });
319
320         final Button button = new Button(parent, SWT.FLAT);
321         button.setText(Messages.GraphDebugger_Lookup);
322         button.setEnabled(false);
323         GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(false, false).applyTo(button);
324
325         text.addKeyListener(new KeyAdapter() {
326             
327             @Override
328             public void keyPressed(KeyEvent e) {
329                 if (e.keyCode == 13) {
330                     String input = text.getText();
331                     setLookupInput(input);
332                     button.setFocus();
333                 }
334             }
335         });
336         
337         button.addSelectionListener(new SelectionListener() {
338             @Override
339             public void widgetDefaultSelected(SelectionEvent e) {
340                 widgetSelected(e);
341             }
342             @Override
343             public void widgetSelected(SelectionEvent e) {
344                 String input = text.getText();
345                 setLookupInput(input);
346             }
347         });
348         
349         text.addModifyListener(new ModifyListener() {
350             @Override
351             public void modifyText(ModifyEvent e) {
352                 String input = text.getText().trim();
353                 if (!input.equals(PROMPT_TEXT) && !input.equals("")) //$NON-NLS-1$
354                     button.setEnabled(true);
355                 else
356                     button.setEnabled(false);
357             }
358         });
359     }
360
361     public void setLookupInput(String input) {
362         // There's no harm in trimming out spaces from both ends of the input.
363         input = input.trim();
364
365         SerialisationSupport support = session.getService(SerialisationSupport.class);
366         if (input.startsWith("$")) { //$NON-NLS-1$
367             try {
368                 Resource r = support.getResource(Long.parseLong(input.substring(1)));
369                 changeLocation(r);
370             } catch (NumberFormatException e1) {
371                 // Ignore, may happen for crap input
372                 setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusInvalidPrefixedInput);
373             } catch (Exception e1) {
374                 ErrorLogger.defaultLogError(e1);
375                 setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusResourceIDFailed);
376             }
377             return;
378         }
379
380         String[] parts = input.split("-"); //$NON-NLS-1$
381
382         if (parts.length == 1) {
383             try {
384                 int resourceKey = Integer.parseInt(parts[0].trim());
385                 Resource r = support.getResource(resourceKey);
386                 // Some validation, not enough though
387                 ClusteringSupport cs = session.getService(ClusteringSupport.class);
388                 long cluster = cs.getCluster(r);
389                 if(cluster > 0) {
390                     changeLocation(r);
391                 }
392             } catch (NumberFormatException e1) {
393                 // Ignore, may happen for crap input
394                 setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusInvalidInput);
395             } catch (Exception e1) {
396                 ErrorLogger.defaultLogError(e1);
397                 setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusTransientResourceID);
398             }
399         } else if (parts.length == 2) {
400             try {
401                 int resourceIndex = Integer.parseInt(parts[1]);
402                 long clusterId = Long.parseLong(parts[0]);
403                 ClusteringSupport cs = session.getService(ClusteringSupport.class);
404                 Resource r = cs.getResourceByIndexAndCluster(resourceIndex, clusterId);
405                 changeLocation(r);
406             } catch (NumberFormatException e1) {
407                 // Ignore, may happen for crap input
408                 setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusInvalidInputExpectIdxClusterIds);
409             } catch (Exception e1) {
410                 ErrorLogger.defaultLogError(e1);
411                 setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusIndexLookpuFailed);
412             }
413         }
414
415         // Try to see if the input data is an URI reference
416         try {
417             // First check that the input really is a proper URI.
418             String uri = input;
419             if (!input.equals("http:/") && input.endsWith("/")) //$NON-NLS-1$ //$NON-NLS-2$
420                 uri = input.substring(0, input.length() - 1);
421             new URI(uri);
422             Resource r = session.syncRequest( Queries.resource( uri ) );
423             changeLocation(r);
424             return;
425         } catch (URISyntaxException e) {
426             // Ignore, this is not a proper URI at all.
427         } catch (ResourceNotFoundException e1) {
428             // Ok, this was an URI, but no resource was found.
429             setStatus(DONT_TOUCH, NLS.bind( Messages.GraphDebugger_StatusResourceforURI , input )); 
430             return;
431         } catch (DatabaseException e1) {
432             setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusURIlookupFailed);
433             ErrorLogger.defaultLogError(e1);
434         }
435
436         setStatus(DONT_TOUCH, Messages.GraphDebugger_StatusInvalidInputResIdORURIExpected);
437     }
438
439     public Label createDropLabel(Composite parent) {
440         final Label label = new Label(parent, SWT.BORDER);
441         label.setAlignment(SWT.CENTER);
442         label.setText(Messages.GraphDebugger_LabelDragResource);
443         label.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY));
444         GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).hint(SWT.DEFAULT, 20).span(2, 1).grab(true, false).applyTo(label);
445
446         // Add resource id drop support to the drop-area.
447         DropTarget dropTarget = new DropTarget(label, DND.DROP_LINK | DND.DROP_COPY);
448         dropTarget.setTransfer(new Transfer[] { TextTransfer.getInstance(), ResourceReferenceTransfer.getInstance(), LocalObjectTransfer.getTransfer() });
449         dropTarget.addDropListener(new DropTargetAdapter() {
450             @Override
451             public void dragEnter(DropTargetEvent event) {
452                 event.detail = DND.DROP_LINK;
453                 label.setBackground((Color) resourceManager.get(green));
454                 return;
455             }
456             @Override
457             public void dragLeave(DropTargetEvent event) {
458                 label.setBackground(null);
459             }
460
461             @Override
462             public void drop(DropTargetEvent event) {
463                 label.setBackground(null);
464                 ResourceArray[] data = parseEventData(event);
465                 if (data == null || data.length != 1) {
466                     event.detail = DND.DROP_NONE;
467                     return;
468                 }
469                 final ResourceArray array = data[0];
470                 final Resource r = array.resources[array.resources.length - 1];
471
472                 changeLocation(r);
473             }
474
475             private ResourceArray[] parseEventData(DropTargetEvent event) {
476                 //System.out.println("DATA: " + event.data);
477                 if (event.data instanceof String) {
478                     try {
479                         SerialisationSupport support = session.getService(SerialisationSupport.class);
480                         return ResourceTransferUtils.readStringTransferable(support, (String) event.data).toResourceArrayArray();
481                     } catch (IllegalArgumentException e) {
482                         ErrorLogger.defaultLogError(e);
483                     } catch (DatabaseException e) {
484                         ErrorLogger.defaultLogError(e);
485                     }
486                 }
487                 ResourceArray[] ret = ResourceAdaptionUtils.toResourceArrays(event.data);
488                 if (ret.length > 0)
489                     return ret;
490                 return null;
491             }
492         });
493
494         return label;
495     }
496
497     public Browser createBrowser(Composite parent) {
498         try {
499             browser = new Browser(parent, SWT.WEBKIT);
500         } catch (SWTError e) {
501             //System.out.println("Could not instantiate Browser: " + e.getMessage());
502             browser = new Browser(parent, SWT.NONE);
503         }
504         GridDataFactory.fillDefaults().span(2, 1).grab(true, true).applyTo(browser);
505
506         // Left/right arrows for back/forward
507         browser.addKeyListener(new KeyAdapter() {
508 //              
509 //              @Override
510 //            public void keyPressed(KeyEvent e) {
511 //                      if (e.keyCode == SWT.F5) {
512 //                      refreshBrowser();
513 //                      }
514 //              }
515 //          }
516             @Override
517             public void keyReleased(KeyEvent e) {
518 //                System.out.println("key, char: " + e.keyCode + ", " + (int) e.character + " (" + e.character + ")");
519                 if (e.keyCode == SWT.BS) {
520                     back();
521                 }
522                 
523                 if (e.keyCode == SWT.F5) {
524                         refreshBrowser();
525                 }
526                 
527                 if ((e.stateMask & SWT.ALT) != 0) {
528                     if (e.keyCode == SWT.ARROW_RIGHT)
529                         forward();
530                     if (e.keyCode == SWT.ARROW_LEFT)
531                         back();
532                 }
533             }
534         });
535
536         // Add listener for debugging functionality
537         browser.addLocationListener(new LocationAdapter() {
538             @Override
539             public void changing(LocationEvent event) {
540                 String location = event.location;
541                 if (location.startsWith("simantics:browser")) //$NON-NLS-1$
542                     location = "about:" + location.substring(17); //$NON-NLS-1$
543                 //System.out.println("changing: location=" + location);
544
545                 // Do not follow links that are meant as actions that are
546                 // handled below.
547                 event.doit = false;
548                 if ("about:blank".equals(location)) { //$NON-NLS-1$
549                     // Just changing to the same old blank url is ok since it
550                     // allows the browser to refresh itself.
551                     event.doit = true;
552                 }
553
554                 if (location.startsWith("about:-link")) { //$NON-NLS-1$
555                     String target = location.replace("about:-link", ""); //$NON-NLS-1$ //$NON-NLS-2$
556                     Resource element = links.getRight(target);
557                     if (element == currentElement) {
558                         event.doit = false;
559                         return;
560                     }
561                     changeLocation(element);
562                 } else if (location.startsWith("about:-remove")) { //$NON-NLS-1$
563                     String target = location.replace("about:-remove", ""); //$NON-NLS-1$ //$NON-NLS-2$
564                     String n[] = target.split(STATEMENT_PART_SEPARATOR);
565                     if (n.length != 3)
566                         return;
567
568                     final Resource s = links.getRight(n[0]);
569                     final Resource p = links.getRight(n[1]);
570                     final Resource o = links.getRight(n[2]);
571
572                     // Make sure this is what the use wants.
573                     MessageDialog md = new MessageDialog(
574                             getShell(),
575                             Messages.GraphDebugger_ConfirmActionsDots,
576                             null,
577                             Messages.GraphDebugger_ConfirmActionsDotsMsg,
578                             MessageDialog.QUESTION, new String[] { Messages.GraphDebugger_Cancel, Messages.GraphDebugger_Continue }, 0);
579                     if (md.open() != 1) {
580                         return;
581                     }
582
583                     session.asyncRequest(new WriteRequest() {
584
585                         @Override
586                         public void perform(WriteGraph g) throws DatabaseException {
587                                 try {
588                                         List<Resource> ls = OrderedSetUtils.toList(g, s);
589                                         if(ls.contains(o))
590                                                 OrderedSetUtils.remove(g, s, o);
591                                 } catch (DatabaseException e) {
592                                         
593                                 }
594                                 try {
595                                         List<Resource> ls = ListUtils.toList(g, s);
596                                         if(ls.contains(o))
597                                                 ListUtils.removeElement(g, s, o);
598                                 } catch (DatabaseException e) {
599                                         
600                                 }
601                             g.denyStatement(s, p, o);
602                         }
603
604                     }, parameter -> refreshBrowser()
605                     );
606                 } else if (location.startsWith("about:-edit-value")) { //$NON-NLS-1$
607                     String target = location.replace("about:-edit-value", ""); //$NON-NLS-1$ //$NON-NLS-2$
608                     final Resource o = links.getRight(target);
609
610                     session.asyncRequest(new ReadRequest() {
611
612                         String previousValue;
613
614                         @Override
615                         public void run(ReadGraph graph) throws DatabaseException {
616
617                             previousValue = getResourceName(graph, o);
618                             final StringModifier modifier = graph.adapt(o, StringModifier.class);
619                             getDisplay().asyncExec(new Runnable() {
620                                 @Override
621                                 public void run() {
622                                     InputDialog dialog = new InputDialog(
623                                             getShell(),
624                                             Messages.GraphDebugger_EditValue,
625                                             null,
626                                             previousValue,
627                                             new IInputValidator() {
628                                                 @Override
629                                                 public String isValid(String newText) {
630                                                     return modifier.isValid(newText);
631                                                 }
632                                             }) {
633                                         private static final String DIALOG = "DebuggerEditValueDialog"; //$NON-NLS-1$
634                                         private IDialogSettings dialogBoundsSettings;
635                                         @Override
636                                         protected IDialogSettings getDialogBoundsSettings() {
637                                             if (dialogBoundsSettings == null) {
638                                                 IDialogSettings settings = Activator.getDefault().getDialogSettings();
639                                                 dialogBoundsSettings = settings.getSection(DIALOG);
640                                                 if (dialogBoundsSettings == null)
641                                                     dialogBoundsSettings = settings.addNewSection(DIALOG);
642                                             }
643                                             return dialogBoundsSettings;
644                                         }
645                                         @Override
646                                         protected Point getInitialSize() {
647                                             Point defaultSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
648                                             Point result = super.getInitialSize();
649                                             if (defaultSize.equals(result))
650                                                 return new Point(600, 400);
651                                             return result;
652                                         }
653                                         protected int getShellStyle() {
654                                             return super.getShellStyle() | SWT.RESIZE;
655                                         }
656                                         protected org.eclipse.swt.widgets.Control createDialogArea(Composite parent) {
657                                             Composite composite = (Composite) super.createDialogArea(parent);
658                                             getText().setLayoutData(
659                                                     new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL
660                                                             | GridData.VERTICAL_ALIGN_FILL | GridData.HORIZONTAL_ALIGN_FILL));
661                                             {
662                                                 Label label = new Label(composite, SWT.NONE);
663                                                 label.moveAbove(getText());
664                                                 label.setText(Messages.GraphDebugger_InputNewPropertyValue);
665                                                 GridData data = new GridData(GridData.GRAB_HORIZONTAL
666                                                         | GridData.HORIZONTAL_ALIGN_FILL
667                                                         | GridData.VERTICAL_ALIGN_CENTER);
668                                                 data.widthHint = convertHorizontalDLUsToPixels(IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH);
669                                                 label.setLayoutData(data);
670                                                 label.setFont(parent.getFont());
671                                             }
672                                             return composite;
673                                         }
674                                         protected int getInputTextStyle() {
675                                             return SWT.MULTI | SWT.BORDER;
676                                         }
677                                     };
678                                     int ok = dialog.open();
679                                     if (ok != Dialog.OK)
680                                         return;
681
682                                     final String value = dialog.getValue();
683                                     session.asyncRequest(new WriteRequest() {
684                                         @Override
685                                         public void perform(WriteGraph g) throws DatabaseException {
686                                             //modifier.modify( g, htmlEscape( value ) );
687                                             modifier.modify( g, value );
688                                         }
689                                     }, parameter -> {
690                                         if (parameter != null)
691                                             ErrorLogger.defaultLogError(parameter);
692                                         refreshBrowser();
693                                     });
694                                     return;
695                                 }
696                             });
697                         }
698
699                     }, new ProcedureAdapter<Object>() {
700                         @Override
701                         public void exception(Throwable t) {
702                             ErrorLogger.defaultLogError(t);
703                         }
704                     });
705
706                 }
707             }
708         });
709
710         // Schedule a request that updates the browser content.
711         refreshBrowser();
712         GraphChangeListenerSupport support = session.getService(GraphChangeListenerSupport.class);
713         support.removeListener(changeListener);
714
715         return browser;
716     }
717
718     public void refreshBrowser() {
719         if (currentElement == null)
720             return;
721
722         // Schedule a request that updates the browser content.
723         updater.asyncRequest(new ReadRequest() {
724
725             @Override
726             public void run(ReadGraph graph) throws DatabaseException {
727                 updateContent(graph, currentElement);
728             }
729
730         });
731
732     }
733
734     public Resource getDebuggerLocation() {
735         return currentElement;
736     }
737
738     public void changeLocation(Resource element) {
739         if (currentElement != null) {
740             backHistory.addLast(currentElement);
741         }
742         currentElement = element;
743         forwardHistory.clear();
744
745         refreshBrowser();
746         setStatus(DONT_TOUCH, null);
747         fireHistoryChanged();
748     }
749
750     public void addHistoryListener(HistoryListener l) {
751         historyListeners.add(l);
752     }
753
754     public void removeHistoryListener(HistoryListener l) {
755         historyListeners.remove(l);
756     }
757
758     private void fireHistoryChanged() {
759         for (HistoryListener l : historyListeners)
760             l.historyChanged();
761     }
762
763     public boolean hasBackHistory() {
764         return backHistory.isEmpty();
765     }
766
767     public boolean hasForwardHistory() {
768         return forwardHistory.isEmpty();
769     }
770
771     public void back() {
772         if (backHistory.isEmpty())
773             return;
774
775         forwardHistory.addFirst(currentElement);
776         currentElement = backHistory.removeLast();
777
778         refreshBrowser();
779         fireHistoryChanged();
780     }
781
782     public void forward() {
783         if (forwardHistory.isEmpty())
784             return;
785
786         backHistory.addLast(currentElement);
787         currentElement = forwardHistory.removeFirst();
788
789         refreshBrowser();
790         fireHistoryChanged();
791     }
792
793     protected String toName(Object o) {
794         Class<?> clazz = o.getClass();
795         if (clazz.isArray()) {
796             int length = Array.getLength(o);
797             if (length > RESOURCE_NAME_MAX_LENGTH) {
798                 if (o instanceof byte[]) {
799                     byte[] arr = (byte[]) o;
800                     byte[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
801                     return truncated("byte", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
802                 } else if (o instanceof int[]) {
803                     int[] arr = (int[]) o;
804                     int[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
805                     return truncated("int", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
806                 } else if (o instanceof long[]) {
807                     long[] arr = (long[]) o;
808                     long[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
809                     return truncated("long", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
810                 } else if (o instanceof float[]) {
811                     float[] arr = (float[]) o;
812                     float[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
813                     return truncated("float", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
814                 } else if (o instanceof double[]) {
815                     double[] arr = (double[]) o;
816                     double[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
817                     return truncated("double", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
818                 } else if (o instanceof boolean[]) {
819                     boolean[] arr = (boolean[]) o;
820                     boolean[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
821                     return truncated("boolean", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
822                 } else if (o instanceof Object[]) {
823                     Object[] arr = (Object[]) o;
824                     Object[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
825                     return truncated("Object", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
826                 } else {
827                     return "Unknown big array " + o.getClass(); //$NON-NLS-1$
828                 }
829             } else {
830                 return o.getClass().getComponentType() + "[" + length + "] = " + ObjectUtils.toString(o); //$NON-NLS-1$ //$NON-NLS-2$
831             }
832         }
833         return null;
834     }
835
836     protected String truncated(String type, String string, int originalLength) {
837         return type + "[" + RESOURCE_NAME_MAX_LENGTH + "/" + originalLength + "] = " + string; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
838     }
839     
840     public static String htmlEscape(String s)
841     {
842         return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br/>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
843     }
844
845     /**
846      * Get resource name(?) 
847      * 
848      * @param graph
849      * @param r
850      * @return
851      */
852     protected String getResourceName(ReadGraph graph, Resource r) {
853         try {
854
855             String name = null;
856             //System.out.println("hasValue(" + NameUtils.getSafeName(graph, r, true));
857             if (graph.hasValue(r)) {
858                 // too large array may cause application to run out of memory.
859                 //System.out.println("getValue(" + NameUtils.getSafeName(graph, r, true));
860                 Datatype type = graph.getPossibleRelatedValue(r, L0.HasDataType, Bindings.getBindingUnchecked(Datatype.class));
861                 if (type!=null) {
862                         Binding rviBinding = graph.getService(Databoard.class).getBindingUnchecked( RVI.class );
863                         if (type.equals( rviBinding.type() )) {
864                                 RVI rvi = graph.getValue(r, rviBinding);
865                                                                 
866                                 try {
867                                         Variable v = Variables.getConfigurationContext( graph, currentElement );
868                                         name = rvi.asString(graph, v);
869 //                                      name = rvi.resolve(graph, v).getURI(graph);
870                                 } catch (DatabaseException dbe ) {
871                                         name = rvi.toString( graph );
872                                 }
873                         } else {
874                                 long valueSize = NameUtils.getPossibleValueSize(graph, r);
875                                 if (valueSize > RESOURCE_NAME_MAX_LENGTH) {
876 //                                      Binding b = Bindings.getBinding(type);
877 //                                      Object v = graph.getValue(r, b);
878 //                                      Serializer s = Bindings.getSerializerUnchecked(b);
879 //                                      int size = s.getSize(v);
880                                     name = "Approx. " + valueSize + " byte literal of type " + type.toSingleLineString(); //$NON-NLS-1$ //$NON-NLS-2$
881                                 } else {                                        
882                                         Binding b = Bindings.getBinding(type);
883                                         Object v = graph.getValue(r, b);
884                                         if (b.type() instanceof StringType) {
885                                                 name = (String) graph.getValue(r, b);
886                                         } else {
887                                             name = b.toString(v, false);
888                                         }
889                                     if (type instanceof ArrayType){
890                                         name = name.substring(1, name.length()-1);
891                                     }
892                                 }
893                         }
894                 } else {
895                     Object o = graph.getValue(r);
896                     name = toName(o);
897                 }
898                 
899                 if(name.isEmpty()) {
900                         name = "<empty value>"; //$NON-NLS-1$
901                 }
902                 
903             }
904             // Does resource have a file ??
905             if (name == null) {
906 //                try {
907 //                    Accessor accessor = graph.getAccessor(r);
908 //                    name = "File of type " + accessor.type().toSingleLineString();
909 //                } catch (DatabaseException e) {
910 //                    // No file, try next alternative.
911 //                }
912             }
913             if (name == null) {
914                 //name = graph.adapt(r, String.class);
915                 //if(name.isEmpty())
916                 name = DebugUtils.getSafeLabel(graph, r);
917                 if (name.isEmpty())
918                     name = "<empty name>"; //$NON-NLS-1$
919             }
920 //            ClusteringSupport support = graph.getSession().getService(ClusteringSupport.class);
921 //            if(name == null)
922 //                return "[" + r.getResourceId() + " - " + support.getCluster(r) + "]";
923             if(displayClusters) {
924 //                SessionDebug debug = graph.getSession().getDebug();
925 //                name += " (" + debug.getCluster(r) + ")";
926             }
927             return name;
928         } catch (AdaptionException e) {
929 //            e.printStackTrace();
930             String name = safeReadableString(graph, r);
931 //          try {
932 //                MessageService.defaultLog(new DetailStatus(IDetailStatus.DEBUG, Activator.PLUGIN_ID, 0, NLS.bind(Messages.Name_adaption_problem, MessageUtil.resource(session, r, "this resource")), e));
933 //            } catch (ReferenceSerializationException e1) {
934 //                e1.printStackTrace();
935 //                ErrorLogger.defaultLogWarning(e1);
936 //            }
937             return name;
938         } catch (Exception e) {
939             ErrorLogger.defaultLogError(e);
940             String name = safeReadableString(graph, r);
941 //          try {
942 //                MessageService.defaultLog(new DetailStatus(IDetailStatus.DEBUG, Activator.PLUGIN_ID, 0, NLS.bind(Messages.Name_formulation_problem, MessageUtil.resource(session, r, "this resource")), e));
943 //            } catch (ReferenceSerializationException e1) {
944 //                e1.printStackTrace();
945 //                ErrorLogger.defaultLogWarning(e1);
946 //            }
947             return name;
948         }
949     }
950
951     private String getResourceRef(ReadGraph graph, Resource r) throws DatabaseException {
952         String name;
953         try {
954             Layer0 L0 = Layer0.getInstance(graph);
955             if (graph.isInstanceOf(r, L0.Assertion)) {
956                 Resource pred = graph.getPossibleObject(r, L0.HasPredicate);
957                 // Don't know how I encountered this but it seems to be possible in some cases..
958                 // Resource obj = graph.getSingleObject(r, L0.HasObject);
959                 Resource obj = graph.getPossibleObject(r, L0.HasObject);
960                 String tmp = htmlEscape( (pred == null ? "No predicate ?" : getResourceName(graph, pred)) + " -> " + (obj == null ? "No object ?" : getResourceName(graph, obj)) + " (Assertion)" ); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
961                 name = tmp.substring(0, Math.min(80, tmp.length()));
962             } else {
963                 String resourceName = getResourceName(graph, r);
964                 if(resourceName.equals("Inverse")) { //$NON-NLS-1$
965                     Resource inverse = graph.getPossibleInverse(r);
966                     if(inverse != null && graph.hasStatement(inverse, L0.ConsistsOf, r))
967                         resourceName = getResourceName(graph, inverse) + "/Inverse"; //$NON-NLS-1$
968                 }
969                 String tmp = htmlEscape( resourceName );
970                 name = tmp.substring(0, Math.min(80, tmp.length()));
971             }
972             
973         } catch (OutOfMemoryError e) {
974             name = "OutOfMemoryError"; //$NON-NLS-1$
975         }
976         String ret = "<a href=\"simantics:browser-link" + getLinkString(r) + "\">" //$NON-NLS-1$ //$NON-NLS-2$
977         + name
978         + "</a>"; //$NON-NLS-1$
979         if (graph.isInstanceOf(r, L0.Literal)) {
980             ret += "&nbsp;<a class=\"edit-link\" href=\"simantics:browser-edit-value" + getLinkString(r) + "\">" //$NON-NLS-1$ //$NON-NLS-2$
981             + "(edit)" //$NON-NLS-1$
982             + "</a>"; //$NON-NLS-1$
983         }
984         return ret;
985     }
986
987     private String getStatementRemoveRef(Resource s, Resource p, Resource o) {
988         return "<a href=\"simantics:browser-remove" + getStatementString(s, p, o) //$NON-NLS-1$
989         + "\" title=\"Remove this statement\">X</a>"; //$NON-NLS-1$
990     }
991
992     private void updatePred(StringBuffer content, ReadGraph graph, Resource subj, Resource pred, List<Resource[]> stats) throws DatabaseException {
993         // Generate output content from statements
994         String[][] objects = new String[stats.size()][];
995         for (int i = 0; i < stats.size(); ++i) {
996             Resource stmSubject = stats.get(i)[0];
997             Resource object = stats.get(i)[1];
998
999             objects[i] = new String[4];
1000             objects[i][0] = getLinkString(object);
1001             objects[i][1] = htmlEscape( getResourceName(graph, object) );
1002             objects[i][2] = getResourceRef(graph, object);
1003
1004             // Make a note if the statement was acquired.
1005             if(!stmSubject.equals(subj)) {
1006                 objects[i][3] = " (in " + getResourceRef(graph, stmSubject) + ")"; //$NON-NLS-1$ //$NON-NLS-2$
1007             }
1008         }
1009
1010         // Sort statements by object name
1011         Arrays.sort(objects, new Comparator<String[]>() {
1012             @Override
1013             public int compare(String[] o1, String[] o2) {
1014                 return AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(o1[1], o2[1]);
1015             }
1016         });
1017
1018         // Output table rows
1019         for (int i = 0; i < objects.length; ++i) {
1020             content.append("<tr>"); //$NON-NLS-1$
1021             // Predicate column
1022             if (i == 0)
1023                 content.append("<td rowspan=\"").append(objects.length).append("\" valign=\"top\">").append(getResourceRef(graph, pred)).append("</td>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
1024
1025             // Object column
1026             if (objects[i][3] == null) content.append("<td>"); //$NON-NLS-1$
1027             else content.append("<td class=\"acquired\">"); //$NON-NLS-1$
1028
1029             content.append(objects[i][2]);
1030             if (objects[i][3] != null)
1031                 content.append(objects[i][3]);
1032
1033             content.append("</td>"); //$NON-NLS-1$
1034             
1035             VirtualGraphSupport vgs = graph.getService(VirtualGraphSupport.class);
1036             VirtualGraph vg = vgs.getGraph(graph, subj, pred, links.getRight(objects[i][0]));
1037             
1038             if(vg != null) {
1039                 content.append("<td>").append(vg.toString()).append("</td>"); //$NON-NLS-1$ //$NON-NLS-2$
1040             } else {
1041                 content.append("<td>DB</td>"); //$NON-NLS-1$
1042             }
1043             
1044
1045             // Statement remove -link column
1046             // Only allowed for non-acquired statements.
1047             if (objects[i][3] == null) {
1048                 content.append("<td class=\"remove\">"); //$NON-NLS-1$
1049                 content.append(getStatementRemoveRef(subj, pred, links.getRight(objects[i][0])));
1050                 content.append("</td>"); //$NON-NLS-1$
1051             }
1052             content.append("</tr>"); //$NON-NLS-1$
1053         }
1054     }
1055
1056     private void updateTag(StringBuffer content, ReadGraph graph, Resource subj, Resource tag) throws DatabaseException {
1057
1058         // Generate output content from statements
1059         String ref = getResourceRef(graph, tag);
1060
1061         content.append("<tr>"); //$NON-NLS-1$
1062         content.append("<td rowspan=\"1\" colspan=\"3\" valign=\"top\">").append(ref).append("</td>"); //$NON-NLS-1$ //$NON-NLS-2$
1063         //content.append("<td>" + name + "</td>");
1064         content.append("<td class=\"remove\">"); //$NON-NLS-1$
1065         content.append(getStatementRemoveRef(subj, tag, subj));
1066         content.append("</td>"); //$NON-NLS-1$
1067         content.append("</tr>"); //$NON-NLS-1$
1068
1069     }
1070
1071     private void updateOrderedSet(StringBuffer content, ReadGraph graph, Resource subj) throws DatabaseException {
1072         //List<Resource> list = OrderedSetUtils.toList(graph, subj);
1073         /*
1074         // Generate output content from statements
1075         String[][] objects = new String[stats.size()][];
1076         for (int i = 0; i < stats.size(); ++i) {
1077             Resource stmSubject = stats.get(i)[0];
1078             Resource object = stats.get(i)[1];
1079
1080             objects[i] = new String[4];
1081             objects[i][0] = getLinkString(object);
1082             objects[i][1] = getResourceName(graph, object);
1083             objects[i][2] = getResourceRef(graph, object);
1084
1085             // Make a note if the statement was acquired.
1086             if(!stmSubject.equals(subj)) {
1087                 objects[i][3] = " (acquired from " + getResourceRef(graph, stmSubject) + ")";
1088             }
1089         }
1090
1091         // Sort statements by object name
1092         Arrays.sort(objects, new Comparator<String[]>() {
1093             @Override
1094             public int compare(String[] o1, String[] o2) {
1095                 return o1[1].compareTo(o2[1]);
1096             }
1097         });*/
1098
1099         List<String> list = new ArrayList<String>();
1100         Resource cur = subj;
1101         while(true) {
1102             try {
1103                 cur = OrderedSetUtils.next(graph, subj, cur);
1104             } catch(DatabaseException e) {
1105                 list.add("<span style=\"color:red;font-weight:bold\">BROKEN ORDERED SET:<br/></span><span style=\"color:red\">" + e.getMessage() + "</span>"); //$NON-NLS-1$ //$NON-NLS-2$
1106                 Resource inv = graph.getPossibleInverse(subj);
1107                 for(Statement stat : graph.getStatements(cur, L0.IsRelatedTo)) {
1108                     if(stat.getSubject().equals(cur)) {
1109                         if(stat.getPredicate().equals(subj)) {
1110                             list.add("next " + getResourceRef(graph, stat.getObject())); //$NON-NLS-1$
1111                         }
1112                         else if(stat.getPredicate().equals(inv)) {
1113                             list.add("prev " + getResourceRef(graph, stat.getObject())); //$NON-NLS-1$
1114                         }
1115                     }
1116                 }
1117                 break;
1118             }
1119             if(cur.equals(subj))
1120                 break;
1121             list.add(getResourceRef(graph, cur));
1122         }
1123
1124         // Output table rows
1125         for (int i = 0; i < list.size() ; ++i) {
1126             content.append("<tr>"); //$NON-NLS-1$
1127             // Predicate column
1128             if (i == 0)
1129                 content.append("<td rowspan=\"").append(list.size()).append("\" valign=\"top\">Ordered Set Elements</td>"); //$NON-NLS-1$ //$NON-NLS-2$
1130
1131             // Object column
1132             content.append("<td>"); //$NON-NLS-1$
1133             content.append(list.get(i));
1134             content.append("</td>"); //$NON-NLS-1$
1135
1136             // Statement remove -link column
1137             // Only allowed for non-acquired statements.
1138             /*if (objects[i][3] == null) {
1139                 content.append("<td class=\"remove\">");
1140                 content.append(getStatementRemoveRef(subj, pred, links.getRight(objects[i][0])));
1141                 content.append("</td>");
1142             }*/
1143             content.append("</tr>"); //$NON-NLS-1$
1144         }
1145     }
1146
1147     private void updateLinkedList(StringBuffer content, ReadGraph graph, Resource subj) throws DatabaseException {
1148
1149         List<String> list = new ArrayList<String>();
1150         
1151         try {
1152                 List<Resource> resources = ListUtils.toList(graph, subj);
1153                 for(Resource element : resources) {
1154                     list.add(getResourceRef(graph, element));
1155                 }
1156         } catch (DatabaseException e) {
1157                 throw new ValidationException(e);
1158         }
1159
1160         // Output table rows
1161         for (int i = 0; i < list.size() ; ++i) {
1162             content.append("<tr>"); //$NON-NLS-1$
1163             // Predicate column
1164             if (i == 0)
1165                 content.append("<td rowspan=\"").append(list.size()).append("\" valign=\"top\">Linked List Elements</td>"); //$NON-NLS-1$ //$NON-NLS-2$
1166
1167             // Object column
1168             content.append("<td>"); //$NON-NLS-1$
1169             content.append(list.get(i));
1170             content.append("</td><td>DB</td>"); //$NON-NLS-1$
1171
1172             // Statement remove -link column
1173             // Only allowed for non-acquired statements.
1174             /*if (objects[i][3] == null) {
1175                 content.append("<td class=\"remove\">");
1176                 content.append(getStatementRemoveRef(subj, pred, links.getRight(objects[i][0])));
1177                 content.append("</td>");
1178             }*/
1179             content.append("</tr>"); //$NON-NLS-1$
1180         }
1181     }
1182
1183     protected synchronized void updateContent(final ReadGraph graph, Resource... resources) throws DatabaseException {
1184         L0 = Layer0.getInstance(graph);
1185
1186         links.clear();
1187         StringBuffer content = new StringBuffer();
1188
1189         // Generate HTML -page
1190         content.append("<html>\n<head>\n") //$NON-NLS-1$
1191         .append(getHead())
1192         .append("\n</head>\n") //$NON-NLS-1$
1193         .append("<body>\n") //$NON-NLS-1$
1194         .append("<div id=\"mainContent\">\n\n"); //$NON-NLS-1$
1195
1196         for (Resource r : resources) {
1197             if (r == null)
1198                 continue;
1199
1200             String uri = null;
1201             try {
1202                 uri = graph.syncRequest(new ResourceToPossibleURI(r));
1203             } catch (Exception e) {
1204                 ErrorLogger.defaultLogError(e);
1205                 uri = "Cannot get URI: " + e.getMessage(); //$NON-NLS-1$
1206             }
1207
1208             // Top DIV
1209             content.append("<div id=\"top\">\n"); //$NON-NLS-1$
1210             content.append("<table class=\"top\">\n"); //$NON-NLS-1$
1211             if (uri != null) {
1212                 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$
1213             }
1214
1215             XSupport xs = graph.getService(XSupport.class);
1216             boolean immutable = xs.getImmutable(r);
1217
1218             Collection<Statement> statements = graph.getStatements(r, L0.IsWeaklyRelatedTo);
1219             HashMultiMap<Resource, Resource[]> map = new HashMultiMap<Resource, Resource[]>();
1220             for(org.simantics.db.Statement statement : statements) {
1221                 Resource predicate = null;
1222                 Resource subject = null;
1223                 Resource obj = null;
1224                 try {
1225                     predicate = statement.getPredicate();
1226                     subject = statement.getSubject();
1227                     obj = statement.getObject();
1228                     map.add(predicate, new Resource[] {subject, obj});
1229                 } catch (Throwable e) {
1230                     ErrorLogger.defaultLogError("Cannot find statement " + subject + " " + predicate + " " + obj, e); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
1231                 }
1232             }
1233             SerialisationSupport ss = graph.getSession().getService(SerialisationSupport.class);
1234             ClusteringSupport support = graph.getSession().getService(ClusteringSupport.class);
1235             content.append("<tr><td class=\"top_key\">Identifiers</td><td class=\"top_value\">"); //$NON-NLS-1$
1236             content.append("<span id=\"resource_id\">") //$NON-NLS-1$
1237             .append(" RID = $").append(r.getResourceId()) //$NON-NLS-1$
1238             .append(" Resource Key = ").append(ss.getTransientId(r)) //$NON-NLS-1$
1239             .append(" CID = ").append(support.getCluster(r)); //$NON-NLS-1$
1240             content.append("</span></td>"); //$NON-NLS-1$
1241             if (immutable)
1242                 content.append("<td class=\"remove\">[IMMUTABLE]</td>"); //$NON-NLS-1$
1243             content.append("</tr>\n"); //$NON-NLS-1$
1244  
1245             boolean isClusterSet = support.isClusterSet(r);
1246             Resource parentSet = support.getClusterSetOfCluster(r);
1247             String parentSetURI = parentSet != null ? graph.getPossibleURI(parentSet) : null;
1248             
1249             content.append("<tr><td class=\"top_key\">Clustering</td><td class=\"top_value\">"); //$NON-NLS-1$
1250             content.append("<span id=\"resource_id\">"); //$NON-NLS-1$
1251             
1252             if(parentSetURI != null)
1253                 content.append(" Containing cluster set = ").append(parentSetURI); //$NON-NLS-1$
1254             else if (parentSet != null)
1255                 content.append(" Containing cluster set = ").append(parentSet.toString()); //$NON-NLS-1$
1256             else 
1257                 content.append(" Not in any cluster set "); //$NON-NLS-1$
1258             
1259             content.append("</span></td>"); //$NON-NLS-1$
1260             if (isClusterSet)
1261                 content.append("<td class=\"remove\">[CLUSTER SET]</td>"); //$NON-NLS-1$
1262             content.append("</tr>\n"); //$NON-NLS-1$
1263             
1264             // If the resource has a value, show it.
1265             String resourceValue = getResourceValue(graph, r);
1266             if (resourceValue != null) {
1267                 content
1268                 .append("<tr><td class=\"top_key\">Attached value</td><td class=\"top_value\">") //$NON-NLS-1$
1269                 .append(htmlEscape(resourceValue))
1270                 .append("</td></tr>\n"); //$NON-NLS-1$
1271             }
1272
1273             // Close #top
1274             content.append("</table>\n"); //$NON-NLS-1$
1275             content.append("</div>\n"); //$NON-NLS-1$
1276
1277             content.append("\n<div id=\"data\">\n"); //$NON-NLS-1$
1278             content.append("<table>\n") //$NON-NLS-1$
1279             .append("<tr><th>Predicate</th><th>Object</th><th>Graph</th></tr>") //$NON-NLS-1$
1280             .append("<tr><td class=\"subtitle\" colspan=\"3\">Basic information</td></tr>"); //$NON-NLS-1$
1281
1282             boolean isOrderedSet = graph.isInstanceOf(r, L0.OrderedSet);
1283             boolean isLinkedList = graph.isInstanceOf(r, L0.List);
1284 //            map.remove(r);
1285
1286             // BASIC INFORMATION:
1287             for (Resource pred :
1288                 new Resource[] {L0.HasName, L0.InstanceOf,
1289                     L0.Inherits, L0.SubrelationOf,
1290                     L0.PartOf, L0.ConsistsOf})
1291                 if (map.containsKey(pred))
1292                     updatePred(content, graph, r, pred, map.remove(pred));
1293
1294             // TAGS
1295             content.append("<tr><td class=\"subtitle\" colspan=\"3\">Tags</td></tr>"); //$NON-NLS-1$
1296             for(Statement stm : statements) {
1297                 if(stm.getSubject().equals(stm.getObject())) {
1298                     updateTag(content, graph, r, stm.getPredicate());
1299                     map.remove(stm.getPredicate());
1300                 }
1301             }
1302
1303             // ORDERED SETS
1304             content.append("<tr><td class=\"subtitle\" colspan=\"3\">Ordered Sets</td></tr>"); //$NON-NLS-1$
1305             for(Statement stm : statements) {
1306                 Resource predicate = stm.getPredicate();
1307                 if(graph.isInstanceOf(stm.getPredicate(), L0.OrderedSet)) {
1308                     updateTag(content, graph, r, stm.getPredicate());
1309                     if(map.get(stm.getPredicate()) != null && map.get(stm.getPredicate()).size() == 1)
1310                         map.remove(stm.getPredicate());
1311                 }
1312                 Resource inverse = graph.getPossibleInverse(predicate);
1313                 if (inverse != null) {
1314                     if(graph.isInstanceOf(inverse, L0.OrderedSet)) {
1315                         if(map.get(stm.getPredicate()) != null && map.get(stm.getPredicate()).size() == 1)
1316                             map.remove(stm.getPredicate());
1317                     }
1318                 } else {
1319                     // FIXME : should we infor missing inverse
1320                 }
1321             }
1322
1323             // IS RELATED TO
1324             content.append("<tr><td class=\"subtitle\" colspan=\"3\">Is Related To</td></tr>"); //$NON-NLS-1$
1325
1326             // ELEMENTS OF ORDERED SET
1327             if(isOrderedSet) {
1328                 //content.append("<tr><td class=\"subtitle\" colspan=\"2\">Ordered set</td></tr>");
1329                 try {
1330                     updateOrderedSet(content, graph, r);
1331                 } catch (ValidationException e) {
1332                     content.append("<td colspan=\"3\"><span style=\"color:red;font-weight:bold\">BROKEN ORDERED SET:<br/></span><span style=\"color:red\">").append(e.getMessage()).append("</span></td>"); //$NON-NLS-1$ //$NON-NLS-2$
1333                 }
1334             }
1335
1336             // ELEMENTS OF LINKED LIST
1337             if(isLinkedList) {
1338                 //content.append("<tr><td class=\"subtitle\" colspan=\"2\">Ordered set</td></tr>");
1339                 try {
1340                     updateLinkedList(content, graph, r);
1341                 } catch (ValidationException e) {
1342                     content.append("<td colspan=\"3\"><span style=\"color:red;font-weight:bold\">BROKEN LINKED LIST:<br/></span><span style=\"color:red\">").append(e.getMessage()).append("</span></td>"); //$NON-NLS-1$ //$NON-NLS-2$
1343                 }
1344             }
1345
1346             // IS RELATED TO (other)
1347             Resource[] preds = map.keySet().toArray(new Resource[0]);
1348             final Map<Resource, String> strmap = new HashMap<Resource, String>(preds.length);
1349             for(Resource pred : preds) {
1350                 String str = htmlEscape( getResourceName(graph, pred) );
1351                 if(str == null)
1352                     str = "<null>"; //$NON-NLS-1$
1353                 strmap.put(pred, str);
1354             }
1355             Arrays.sort(preds, new Comparator<Resource>() {
1356                 @Override
1357                 public int compare(Resource o1, Resource o2) {
1358                     return AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(strmap.get(o1), strmap.get(o2));
1359                 }
1360             });
1361             for(Resource pred : preds)
1362                 if(graph.isSubrelationOf(pred, L0.IsRelatedTo))
1363                     updatePred(content, graph, r, pred, map.get(pred));
1364
1365             // OTHER STATEMENTS
1366             content.append("<tr><td class=\"subtitle\" colspan=\"3\">Other statements</td></tr>"); //$NON-NLS-1$
1367             for(Resource pred : preds)
1368                 if(!graph.isSubrelationOf(pred, L0.IsRelatedTo))
1369                     updatePred(content, graph, r, pred, map.get(pred));
1370             content.append("</table>\n"); //$NON-NLS-1$
1371         }
1372         // Close #data
1373         content.append("</div>\n\n"); //$NON-NLS-1$
1374         // Close #mainContent
1375         content.append("</div>\n"); //$NON-NLS-1$
1376         content.append("</body>\n</html>\n"); //$NON-NLS-1$
1377
1378         // Update content
1379         final String finalContent = content.toString();
1380         if (!isDisposed()) {
1381             getDisplay().asyncExec(new Runnable() {
1382                 @Override
1383                 public void run() {
1384                     if (!browser.isDisposed())
1385                         browser.setText(finalContent);
1386                 }
1387             });
1388         }
1389     }
1390
1391     private String getResourceValue(ReadGraph graph, Resource r) {
1392         try {
1393             if (graph.hasValue(r)) {
1394                 // too large array may cause application to run out of memory.
1395                 //System.out.println("getValue(" + NameUtils.getSafeName(graph, r, true));
1396                 Datatype type = graph.getPossibleRelatedValue(r, L0.HasDataType, Bindings.getBindingUnchecked(Datatype.class));
1397                 if (type != null) {
1398                     Binding rviBinding = graph.getService(Databoard.class).getBindingUnchecked( RVI.class );
1399                     if (type.equals( rviBinding.type() )) {
1400                         RVI rvi = graph.getValue(r, rviBinding);
1401                         try {
1402                             Variable v = Variables.getConfigurationContext( graph, r );
1403                             return rvi.asString(graph, v);
1404                         } catch (DatabaseException dbe ) {
1405                             return rvi.toString( graph );
1406                         }
1407                     } else {
1408                         Binding b = Bindings.getBinding(type);
1409                         Object v = graph.getValue(r, b);
1410                         Serializer s = Bindings.getSerializerUnchecked(b);
1411                         int size = s.getSize(v);
1412                         if (size > RESOURCE_VALUE_MAX_SIZE) {
1413                             return "Approx. " + size + " byte literal of type " + type.toSingleLineString(); //$NON-NLS-1$ //$NON-NLS-2$
1414                         } else {
1415                             return b.toString(v, false);
1416                         }
1417                     }
1418                 } else {
1419                     Object o = graph.getValue(r);
1420                     return toName(o);
1421                 }
1422             }
1423             return null;
1424         } catch (DatabaseException e) {
1425             return e.getMessage();
1426         } catch (IOException e) {
1427             return e.getMessage();
1428         } catch (BindingException e) {
1429             return e.getMessage();
1430         }
1431     }
1432
1433     private static String safeReadableString(ReadGraph g, Resource r) {
1434         try {
1435             return NameUtils.getSafeName(g, r);
1436         } catch(Throwable throwable) {
1437             ErrorLogger.defaultLogError(throwable);
1438             return "<font color=\"red\"><i>"+throwable.getClass().getName()+"</i> "+throwable.getMessage()+"</font>"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
1439         }
1440     }
1441
1442 //    private static String safeReadableString(IEntity t) {
1443 //        try {
1444 //            return ResourceDebugUtils.getReadableNameForEntity(t);
1445 //        } catch(Throwable throwable) {
1446 //            throwable.printStackTrace();
1447 //            return "<font color=\"red\"><i>"+throwable.getClass().getName()+"</i> "+throwable.getMessage()+"</font>";
1448 //        }
1449 //    }
1450
1451     private String getLinkString(Container<Resource> t) {
1452         String link = links.getLeft(t.get());
1453         if(link == null) {
1454             link = UUID.randomUUID().toString();
1455             links.map(link, t.get());
1456         }
1457         return link;
1458     }
1459
1460 //    private String getPropertyEditString(Container<Resource> t) {
1461 //        String link = links.getLeft(t.get());
1462 //        if(link == null) {
1463 //            link = UUID.randomUUID().toString();
1464 //            links.map(link, t.get());
1465 //        }
1466 //        return link;
1467 //    }
1468
1469     private String getStatementString(Resource _s, Resource _p, Resource _o) {
1470         String s = getLinkString(_s);
1471         String p = getLinkString(_p);
1472         String o = getLinkString(_o);
1473         return s + STATEMENT_PART_SEPARATOR + p + STATEMENT_PART_SEPARATOR + o;
1474     }
1475
1476 //    private String getStatementString(Statement stm) {
1477 //        String s = getLinkString(stm.getSubject());
1478 //        String p = getLinkString(stm.getPredicate());
1479 //        String o = getLinkString(stm.getObject());
1480 //        return s + STATEMENT_PART_SEPARATOR + p + STATEMENT_PART_SEPARATOR + o;
1481 //    }
1482
1483 //    private String toOutgoingTableRow(IEntity subject, Statement stm) {
1484 //        boolean isAcquired = !subject.equals(stm.getSubject());
1485 //
1486 //        String plainCell = "%s";
1487 //        String hrefCell = "<a href=\"%s\">%s</a>";
1488 //        String formatTemplate = isAcquired
1489 //                ? "<tr>\n<td class=\"acquired\">{0}</td>\n<td class=\"acquired\">{1}</td>\n<td class=\"acquired\">{1}</td>\n<td class=\"acquired\">{1}</td>\n</tr>"
1490 //                : "<tr>\n<td>{1}</td>\n<td>{1}</td>\n<td>{1}</td>\n<td>{1}</td>\n</tr>";
1491 //        String format = NLS.bind(formatTemplate, plainCell, hrefCell);
1492 ////        System.out.println("format: " + format);
1493 //
1494 //        IEntity s = stm.getSubject();
1495 //        IEntity p = stm.getPredicate();
1496 //        IEntity o = stm.getObject();
1497 //
1498 ////        String timePart = "[" + timeToString(t.getBegin()) + ", " + timeToString(t.getEnd()) + "]";
1499 //
1500 //        try {
1501 //            return !isAcquired
1502 //                ? String.format(format,
1503 //                    "about:blank-remove" + getStatementString(stm), "Remove",
1504 //                    "about:blank-link" + getLinkString(s), safeReadableString(s),
1505 //                    "about:blank-link" + getLinkString(p), safeReadableString(p),
1506 //                    "about:blank-link" + getLinkString(o), safeReadableString(o))
1507 //                : String.format(format,
1508 //                    "Acquired",
1509 //                    "about:blank-link" + getLinkString(s), safeReadableString(s),
1510 //                    "about:blank-link" + getLinkString(p), safeReadableString(p),
1511 //                    "about:blank-link" + getLinkString(o), safeReadableString(o)
1512 //            );
1513 //        } catch (Throwable throwable) {
1514 //            return "<tr><td colspan=\"4\"><font color=\"red\"><i>"+throwable.getClass().getName()+"</i> "+throwable.getMessage()+"</font></td></tr>";
1515 //        }
1516 //    }
1517
1518 //    private String intervalToString(Interval time) {
1519 //        return timeToString(time.getBegin()) + ", " + timeToString(time.getEnd());
1520 //    }
1521 //
1522 //    DateFormat dateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
1523 //
1524 //    private String timeToString(long time) {
1525 //        if (time == Long.MIN_VALUE)
1526 //            return "-&infin;";
1527 //        if (time == Long.MAX_VALUE)
1528 //            return "+&infin;";
1529 //        //return String.valueOf(time);
1530 //        Date d = new Date(time);
1531 //        return dateTimeFormat.format(d);
1532 //    }
1533
1534     private String getHead() {
1535         String result = ""; //$NON-NLS-1$
1536         if (cssPath != null) {
1537             result = "<link href=\"" + cssPath + "\" rel=\"stylesheet\" type=\"text/css\">"; //$NON-NLS-1$ //$NON-NLS-2$
1538         }
1539         return result;
1540     }
1541
1542 }