Stop using SWT.MOZILLA
[simantics/platform.git] / bundles / org.simantics.debug.ui / src / org / simantics / debug / ui / VariableDebugger.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2020 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.URL;
19 import java.nio.charset.Charset;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.LinkedList;
23 import java.util.TreeMap;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.atomic.AtomicReference;
26
27 import org.eclipse.core.runtime.Assert;
28 import org.eclipse.core.runtime.FileLocator;
29 import org.eclipse.core.runtime.IPath;
30 import org.eclipse.core.runtime.Path;
31 import org.eclipse.jface.layout.GridDataFactory;
32 import org.eclipse.jface.resource.ColorDescriptor;
33 import org.eclipse.jface.resource.JFaceResources;
34 import org.eclipse.jface.resource.LocalResourceManager;
35 import org.eclipse.swt.SWT;
36 import org.eclipse.swt.SWTError;
37 import org.eclipse.swt.browser.Browser;
38 import org.eclipse.swt.browser.LocationAdapter;
39 import org.eclipse.swt.browser.LocationEvent;
40 import org.eclipse.swt.dnd.DND;
41 import org.eclipse.swt.dnd.DropTarget;
42 import org.eclipse.swt.dnd.DropTargetAdapter;
43 import org.eclipse.swt.dnd.DropTargetEvent;
44 import org.eclipse.swt.dnd.TextTransfer;
45 import org.eclipse.swt.dnd.Transfer;
46 import org.eclipse.swt.events.DisposeEvent;
47 import org.eclipse.swt.events.DisposeListener;
48 import org.eclipse.swt.events.KeyAdapter;
49 import org.eclipse.swt.events.KeyEvent;
50 import org.eclipse.swt.events.SelectionEvent;
51 import org.eclipse.swt.events.SelectionListener;
52 import org.eclipse.swt.graphics.Color;
53 import org.eclipse.swt.graphics.RGB;
54 import org.eclipse.swt.layout.GridData;
55 import org.eclipse.swt.layout.GridLayout;
56 import org.eclipse.swt.widgets.Button;
57 import org.eclipse.swt.widgets.Composite;
58 import org.eclipse.swt.widgets.Label;
59 import org.eclipse.swt.widgets.Text;
60 import org.simantics.databoard.type.Datatype;
61 import org.simantics.databoard.util.ObjectUtils;
62 import org.simantics.db.ReadGraph;
63 import org.simantics.db.Resource;
64 import org.simantics.db.Session;
65 import org.simantics.db.common.ResourceArray;
66 import org.simantics.db.common.procedure.adapter.DisposableListener;
67 import org.simantics.db.common.request.UnaryRead;
68 import org.simantics.db.exception.DatabaseException;
69 import org.simantics.db.layer0.SelectionHints;
70 import org.simantics.db.layer0.request.PossibleURI;
71 import org.simantics.db.layer0.request.ResourceURIToVariable;
72 import org.simantics.db.layer0.request.VariableURI;
73 import org.simantics.db.layer0.variable.AbstractChildVariable;
74 import org.simantics.db.layer0.variable.AbstractPropertyVariable;
75 import org.simantics.db.layer0.variable.Variable;
76 import org.simantics.db.layer0.variable.VariableNode;
77 import org.simantics.db.layer0.variable.Variables;
78 import org.simantics.db.service.SerialisationSupport;
79 import org.simantics.debug.ui.internal.Activator;
80 import org.simantics.layer0.Layer0;
81 import org.simantics.structural2.variables.Connection;
82 import org.simantics.structural2.variables.VariableConnectionPointDescriptor;
83 import org.simantics.ui.dnd.LocalObjectTransfer;
84 import org.simantics.ui.dnd.ResourceReferenceTransfer;
85 import org.simantics.ui.dnd.ResourceTransferUtils;
86 import org.simantics.ui.utils.ResourceAdaptionUtils;
87 import org.simantics.utils.FileUtils;
88 import org.simantics.utils.bytes.Base64;
89 import org.simantics.utils.ui.ErrorLogger;
90 import org.simantics.utils.ui.ISelectionUtils;
91 import org.simantics.utils.ui.PathUtils;
92 import org.slf4j.Logger;
93 import org.slf4j.LoggerFactory;
94
95
96 /**
97  * @author Antti Villberg
98  * @author Tuukka Lehtonen
99  */
100 public class VariableDebugger extends Composite {
101
102     private static final Logger LOGGER = LoggerFactory.getLogger(VariableDebugger.class);
103
104     public interface HistoryListener {
105         void historyChanged();
106     }
107
108     private final static String                         DEFAULT_DEBUGGER_CSS_FILE = "debugger.css"; //$NON-NLS-1$
109     private final static String                         DEFAULT_DEBUGGER_CSS_PATH = "css/" + DEFAULT_DEBUGGER_CSS_FILE; //$NON-NLS-1$
110
111     private static int                                  RESOURCE_NAME_MAX_LENGTH  = 1000;
112
113         private final Charset                               utf8 = Charset.forName("UTF-8"); //$NON-NLS-1$
114
115     private final LocalResourceManager                  resourceManager;
116
117     private String                                      cssPath;
118
119     private Text                                        updateTriggerCounter; 
120     private Browser                                     browser;
121     private final ColorDescriptor                       green = ColorDescriptor.createFrom(new RGB(0x57, 0xbc, 0x95));
122
123     private final LinkedList<String>                    backHistory               = new LinkedList<String>();
124     private final LinkedList<String>                    forwardHistory            = new LinkedList<String>();
125     private String                                      currentElement            = null;
126
127     /**
128      * The Session used to access the graph. Received from outside of this
129      * class and therefore it is not disposed here, just used.
130      */
131     private final Session                               session;
132
133     private final CopyOnWriteArrayList<HistoryListener> historyListeners          = new CopyOnWriteArrayList<HistoryListener>();
134
135     protected Layer0                                    L0;
136
137     protected boolean                                   disposed;
138
139     class PageContentListener extends DisposableListener<String> {
140         int triggerCounter;
141         int updateCount;
142         AtomicReference<String> lastResult = new AtomicReference<String>();
143         @Override
144         public void execute(final String content) {
145             ++triggerCounter;
146             //System.out.println("LISTENER TRIGGERED: " + triggerCounter);
147             //System.out.println("LISTENER:\n" + content);
148             if (lastResult.getAndSet(content) == null) {
149                 if (!disposed) {
150                     getDisplay().asyncExec(new Runnable() {
151                         @Override
152                         public void run() {
153                             String content = lastResult.getAndSet(null);
154                             if (content == null)
155                                 return;
156
157                             ++updateCount;
158                             //System.out.println("UPDATE " + updateCount);
159
160                             if (!browser.isDisposed())
161                                 browser.setText(content);
162                             if (!updateTriggerCounter.isDisposed())
163                                 updateTriggerCounter.setText(updateCount + "/" + triggerCounter); //$NON-NLS-1$
164                         }
165                     });
166                 }
167             }
168         }
169
170         @Override
171         public void exception(Throwable t) {
172             LOGGER.error("Page content listener failed unexpectedly", t);
173         }
174     }
175
176     private PageContentListener pageContentListener;
177
178     /**
179      * @param parent
180      * @param style
181      * @param session
182      * @param resource the initial resource to debug or <code>null</code> for
183      *        initially blank UI.
184      */
185     public VariableDebugger(Composite parent, int style, final Session session, String initialURI) {
186         super(parent, style);
187         Assert.isNotNull(session, "session is null"); //$NON-NLS-1$
188         this.session = session;
189         this.currentElement = initialURI;
190         this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), parent);
191
192         initializeCSS();
193
194         addDisposeListener(new DisposeListener() {
195             @Override
196             public void widgetDisposed(DisposeEvent e) {
197                 disposed = true;
198                 PageContentListener l = pageContentListener;
199                 if (l != null)
200                     l.dispose();
201             }
202         });
203     }
204
205     public void defaultInitializeUI() {
206         setLayout(new GridLayout(4, false));
207         setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
208
209         createDropLabel(this);
210         createResourceText(this);
211         createUpdateTriggerCounter(this);
212         Browser browser = createBrowser(this);
213         GridDataFactory.fillDefaults().span(4, 1).grab(true, true).applyTo(browser);
214     }
215
216     protected void initializeCSS() {
217         // Extract default css to a temporary location if necessary.
218         try {
219             IPath absolutePath = PathUtils.getAbsolutePath(Activator.PLUGIN_ID, DEFAULT_DEBUGGER_CSS_PATH);
220             if (absolutePath != null) {
221                 cssPath = absolutePath.toFile().toURI().toString();
222             } else {
223                 File tempDir = FileUtils.getOrCreateTemporaryDirectory(false);
224                 File css = new File(tempDir, DEFAULT_DEBUGGER_CSS_FILE);
225                 if (!css.exists()) {
226                     URL url = FileLocator.find(Activator.getDefault().getBundle(), new Path(DEFAULT_DEBUGGER_CSS_PATH), null);
227                     if (url == null)
228                         throw new FileNotFoundException("Could not find '" + DEFAULT_DEBUGGER_CSS_PATH + "' in bundle '" + Activator.PLUGIN_ID + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
229                     cssPath = FileUtils.copyResource(url, css, true).toURI().toString();
230                 } else {
231                     cssPath = css.toURI().toString();
232                 }
233             }
234         } catch (IOException e) {
235             e.printStackTrace();
236             // CSS extraction failed, let's just live without it then.
237             ErrorLogger.defaultLogWarning(e);
238         }
239     }
240
241     public Label createDropLabel(Composite parent) {
242         final Label label = new Label(parent, SWT.BORDER | SWT.FLAT);
243         label.setAlignment(SWT.CENTER);
244         label.setText(Messages.VariableDebugger_DragResourceToDebugger);
245         label.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY));
246         GridData data = new GridData(SWT.LEFT, SWT.FILL, false, false);
247         label.setLayoutData(data);
248
249         // Add resource id drop support to the drop-area.
250         DropTarget dropTarget = new DropTarget(label, DND.DROP_LINK | DND.DROP_COPY);
251         dropTarget.setTransfer(new Transfer[] { TextTransfer.getInstance(), ResourceReferenceTransfer.getInstance(), LocalObjectTransfer.getTransfer() });
252         dropTarget.addDropListener(new DropTargetAdapter() {
253             @Override
254             public void dragEnter(DropTargetEvent event) {
255                 event.detail = DND.DROP_LINK;
256                 label.setBackground((Color) resourceManager.get(green));
257                 return;
258             }
259             @Override
260             public void dragLeave(DropTargetEvent event) {
261                 label.setBackground(null);
262             }
263
264             @Override
265             public void drop(DropTargetEvent event) {
266                 label.setBackground(null);
267                 String uri = null;
268                 try {
269                     uri = parseUri(event);
270                     if (uri == null) {
271                         event.detail = DND.DROP_NONE;
272                         return;
273                     }
274                     changeLocation(uri);
275                 } catch (DatabaseException e) {
276                     LOGGER.error("Changing location to URI {} failed", uri, e);
277                 }
278             }
279
280             private String parseUri(DropTargetEvent event) throws DatabaseException {
281                 Variable v = parseVariable(event);
282                 String uri = v != null ? session.sync(new VariableURI(v)) : null;
283                 if (uri == null) {
284                     Resource r = parseResource(event);
285                     uri = r != null ? session.sync(new PossibleURI(r)) : null;
286                 }
287                 return uri;
288             }
289
290             private Variable parseVariable(DropTargetEvent event) {
291                 return ISelectionUtils.getSinglePossibleKey(event.data, SelectionHints.KEY_MAIN, Variable.class);
292             }
293
294             private Resource parseResource(DropTargetEvent event) throws DatabaseException {
295                 ResourceArray[] ra = null;
296                 if (event.data instanceof String) {
297                     try {
298                         SerialisationSupport support = session.getService(SerialisationSupport.class);
299                         ra = ResourceTransferUtils.readStringTransferable(support, (String) event.data).toResourceArrayArray();
300                     } catch (IllegalArgumentException e) {
301                         e.printStackTrace();
302                     } catch (DatabaseException e) {
303                         e.printStackTrace();
304                     }
305                 } else {
306                     ra = ResourceAdaptionUtils.toResourceArrays(event.data);
307                 }
308                 if (ra != null && ra.length > 0)
309                     return ra[0].resources[ra[0].resources.length - 1];
310                 return null;
311             }
312         });
313
314         return label;
315     }
316
317     public void createResourceText(Composite parent) {
318         final Text text = new Text(parent, SWT.BORDER);
319         GridData data = new GridData(SWT.FILL, SWT.FILL, true, false);
320         text.setLayoutData(data);
321
322         Button button = new Button(parent, SWT.NONE);
323         button.setText(Messages.VariableDebugger_Lookup);
324         GridData data2 = new GridData(SWT.FILL, SWT.FILL, false, false);
325         button.setLayoutData(data2);
326
327         button.addSelectionListener(new SelectionListener() {
328
329             @Override
330             public void widgetDefaultSelected(SelectionEvent e) {
331                 widgetSelected(e);
332             }
333
334             @Override
335             public void widgetSelected(SelectionEvent e) {
336
337                 String uri = null;
338                 try {
339                     uri = text.getText();
340                     // Make sure that URI is resolvable to Variable
341                     session.sync(new ResourceURIToVariable(uri));
342                     changeLocation(uri);
343                 } catch (DatabaseException e1) {
344                     LOGGER.error("Lookup failed for URI {}", uri, e1);
345                 }
346
347             }
348
349         });
350     }
351
352     protected Text createUpdateTriggerCounter(Composite parent) {
353         Text label = new Text(parent, SWT.BORDER | SWT.FLAT);
354         label.setEditable(false);
355         label.setToolTipText(Messages.VariableDebugger_TextToolTip);
356         GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
357         .grab(false, false).hint(32, SWT.DEFAULT).applyTo(label);
358         updateTriggerCounter = label;
359         return label;
360     }
361
362     public Browser createBrowser(Composite parent) {
363         try {
364             browser = new Browser(parent, SWT.NONE);
365         } catch (SWTError e) {
366             //System.out.println("Could not instantiate Browser: " + e.getMessage());
367             browser = new Browser(parent, SWT.NONE);
368         }
369         browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
370
371         // Left/right arrows for back/forward
372         browser.addKeyListener(new KeyAdapter() {
373             @Override
374             public void keyReleased(KeyEvent e) {
375 //                System.out.println("key, char: " + e.keyCode + ", " + (int) e.character + " (" + e.character + ")");
376 //                if (e.keyCode == SWT.BS) {
377 //                    back();
378 //                }
379                 if ((e.stateMask & SWT.ALT) != 0) {
380                     if (e.keyCode == SWT.ARROW_RIGHT)
381                         forward();
382                     if (e.keyCode == SWT.ARROW_LEFT)
383                         back();
384                 }
385             }
386         });
387
388         // Add listener for debugging functionality
389         browser.addLocationListener(new LocationAdapter() {
390             @Override
391             public void changing(LocationEvent event) {
392                 String location = event.location;
393                 if (location.startsWith("simantics:browser")) //$NON-NLS-1$
394                     location = "about:" + location.substring(17); //$NON-NLS-1$
395                 //System.out.println("changing: location=" + location);
396
397                 // Do not follow links that are meant as actions that are
398                 // handled below.
399                 event.doit = false;
400                 if ("about:blank".equals(location)) { //$NON-NLS-1$
401                     // Just changing to the same old blank url is ok since it
402                     // allows the browser to refresh itself.
403                     event.doit = true;
404                 }
405
406                 if (location.startsWith("about:-link")) { //$NON-NLS-1$
407                     String target = location.replace("about:-link", ""); //$NON-NLS-1$ //$NON-NLS-2$
408                     try {
409                         byte[] bytes = Base64.decode(target);
410                         String url = new String(bytes, utf8);
411                         if (url.equals(currentElement)) {
412                             event.doit = false;
413                             return;
414                         }
415                         changeLocation(url);
416                     } catch (IOException e) {
417                         ErrorLogger.defaultLogError(e);
418                     }
419                 } else if (location.startsWith("about:-remove")) { //$NON-NLS-1$
420                 } else if (location.startsWith("about:-edit-value")) { //$NON-NLS-1$
421                 }
422             }
423         });
424
425         // Schedule a request that updates the browser content.
426         refreshBrowser();
427
428         return browser;
429     }
430
431     public void refreshBrowser() {
432         if (currentElement == null)
433             return;
434
435         // Schedule a request that updates the browser content.
436         if (pageContentListener != null)
437             pageContentListener.dispose();
438         pageContentListener = new PageContentListener();
439         session.asyncRequest(new UnaryRead<String, String>(currentElement) {
440             @Override
441             public String perform(ReadGraph graph) throws DatabaseException {
442                 String content = calculateContent(graph, parameter);
443                 //System.out.println("HTML: " + content);
444                 return content;
445             }
446         }, pageContentListener);
447
448     }
449
450     public String getDebuggerLocation() {
451         return currentElement;
452     }
453
454     public void changeLocation(String url) {
455         if (currentElement != null) {
456             backHistory.addLast(currentElement);
457         }
458         currentElement = url;
459         forwardHistory.clear();
460
461         refreshBrowser();
462         fireHistoryChanged();
463     }
464
465     public void addHistoryListener(HistoryListener l) {
466         historyListeners.add(l);
467     }
468
469     public void removeHistoryListener(HistoryListener l) {
470         historyListeners.remove(l);
471     }
472
473     private void fireHistoryChanged() {
474         for (HistoryListener l : historyListeners)
475             l.historyChanged();
476     }
477
478     public boolean hasBackHistory() {
479         return backHistory.isEmpty();
480     }
481
482     public boolean hasForwardHistory() {
483         return forwardHistory.isEmpty();
484     }
485
486     public void back() {
487         if (backHistory.isEmpty())
488             return;
489
490         forwardHistory.addFirst(currentElement);
491         currentElement = backHistory.removeLast();
492
493         refreshBrowser();
494         fireHistoryChanged();
495     }
496
497     public void forward() {
498         if (forwardHistory.isEmpty())
499             return;
500
501         backHistory.addLast(currentElement);
502         currentElement = forwardHistory.removeFirst();
503
504         refreshBrowser();
505         fireHistoryChanged();
506     }
507
508     protected String toName(Object o) {
509         Class<?> clazz = o.getClass();
510         if (clazz.isArray()) {
511             int length = Array.getLength(o);
512             if (length > RESOURCE_NAME_MAX_LENGTH) {
513                 if (o instanceof byte[]) {
514                     byte[] arr = (byte[]) o;
515                     byte[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
516                     return truncated("byte", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
517                 } else if (o instanceof int[]) {
518                     int[] arr = (int[]) o;
519                     int[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
520                     return truncated("int", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
521                 } else if (o instanceof long[]) {
522                     long[] arr = (long[]) o;
523                     long[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
524                     return truncated("long", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
525                 } else if (o instanceof float[]) {
526                     float[] arr = (float[]) o;
527                     float[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
528                     return truncated("float", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
529                 } else if (o instanceof double[]) {
530                     double[] arr = (double[]) o;
531                     double[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
532                     return truncated("double", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
533                 } else if (o instanceof boolean[]) {
534                     boolean[] arr = (boolean[]) o;
535                     boolean[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
536                     return truncated("boolean", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
537                 } else if (o instanceof Object[]) {
538                     Object[] arr = (Object[]) o;
539                     Object[] arr2 = Arrays.copyOf(arr, RESOURCE_NAME_MAX_LENGTH);
540                     return truncated("Object", Arrays.toString(arr2), arr.length); //$NON-NLS-1$
541                 } else {
542                     return "Unknown big array " + o.getClass(); //$NON-NLS-1$
543                 }
544             } else {
545                 return o.getClass().getComponentType() + "[" + length + "] = " + ObjectUtils.toString(o); //$NON-NLS-1$ //$NON-NLS-2$
546             }
547         }
548         return null;
549     }
550
551     protected String truncated(String type, String string, int originalLength) {
552         return type + "[" + RESOURCE_NAME_MAX_LENGTH + "/" + originalLength + "] = " + string; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
553     }
554
555     protected String getVariableName(ReadGraph graph, Variable r) {
556         try {
557             return r.getName(graph);
558         } catch (Exception e) {
559             return e.getMessage();
560         }
561     }
562
563     protected String getValue(ReadGraph graph, Variable base, Object o) throws DatabaseException {
564         Class<?> clazz = o.getClass();
565         if(o instanceof Connection) {
566             Connection c = (Connection)o;
567             ArrayList<String> result = new ArrayList<String>();
568             for(VariableConnectionPointDescriptor v : c.getConnectionPointDescriptors(graph, null)) {
569                 result.add(v.getRelativeRVI(graph, base));
570             }
571             return "c " + result.toString(); //$NON-NLS-1$
572         } else if (clazz.isArray()) {
573             if(int[].class == clazz) {
574                 return Arrays.toString((int[])o);
575             } else if(float[].class == clazz) {
576                 return Arrays.toString((float[])o);
577             } else if(double[].class == clazz) {
578                 return Arrays.toString((double[])o);
579             } else if(long[].class == clazz) {
580                 return Arrays.toString((long[])o);
581             } else if(byte[].class == clazz) {
582                 return Arrays.toString((byte[])o);
583             } else if(boolean[].class == clazz) {
584                 return Arrays.toString((boolean[])o);
585             } else if(char[].class == clazz) {
586                 return Arrays.toString((char[])o);
587             } else {
588                 return Arrays.toString((Object[])o);
589             }
590         }
591         return o.toString();
592     }
593     
594     protected String getValue(ReadGraph graph, Variable r) {
595         try {
596             Object value = r.getValue(graph);
597             if(value instanceof Resource) return getResourceRef(graph, (Resource)value);
598             else if (value instanceof Variable) return getVariableRef(graph, (Variable)value);
599             else return value != null ? getValue(graph, r, value) : "null"; //$NON-NLS-1$
600         } catch (Throwable e) {
601             try {
602                 LOGGER.error("getValue({})", r.getURI(graph), e); //$NON-NLS-1$
603             } catch (DatabaseException e1) {
604                 LOGGER.error("Failed to get URI for problematic value variable", e1);
605             }
606             return e.getMessage();
607         }
608     }
609
610     protected String getDatatype(ReadGraph graph, Variable r) {
611         try {
612             Datatype dt = r.getPossibleDatatype(graph);
613             return dt != null ? dt.toSingleLineString() : "undefined"; //$NON-NLS-1$
614         } catch (Exception e) {
615             return e.getMessage();
616         }
617     }
618
619     private String getResourceRef(ReadGraph graph, Resource r) throws DatabaseException {
620         return getVariableRef(graph, graph.adapt(r, Variable.class));
621     }
622
623     private String getVariableRef(ReadGraph graph, Variable r) throws DatabaseException {
624         String ret = "<a href=\"simantics:browser-link" + getLinkString(graph, r) + "\">" //$NON-NLS-1$ //$NON-NLS-2$
625         + getVariableName(graph, r)
626         + "</a>"; //$NON-NLS-1$
627 //        if (graph.isInstanceOf(r, L0.Literal)) {
628 //            ret += "&nbsp;<a class=\"edit-link\" href=\"simantics:browser-edit-value" + getLinkString(r) + "\">"
629 //            + "(edit value)"
630 //            + "</a>";
631 //        }
632         return ret;
633     }
634
635     private String getLinkString(ReadGraph graph, Variable t) throws DatabaseException {
636         try {
637             String uri = t.getURI(graph);
638             //return uri;
639             String encoded = Base64.encode(uri.getBytes(utf8));
640             return encoded;
641         } catch (Exception e) {
642             LOGGER.error("Failed to construct link string for variable", e); //$NON-NLS-1$
643             return e.getMessage();
644         }
645     }
646
647     private void updateProperty(StringBuilder content, ReadGraph graph, Variable property) throws DatabaseException {
648 //        try {
649 //            System.out.println("update property " + property.getURI(graph));
650 //        } catch (Exception e) {
651 //            e.printStackTrace();
652 //        }
653         content.append("<tr>"); //$NON-NLS-1$
654         content.append("<td>").append(getVariableRef(graph, property)).append("</td>"); //$NON-NLS-1$ //$NON-NLS-2$
655         content.append("<td>").append(getValue(graph, property)).append("</td>"); //$NON-NLS-1$ //$NON-NLS-2$
656         content.append("<td>").append(getDatatype(graph, property)).append("</td>"); //$NON-NLS-1$ //$NON-NLS-2$
657         content.append("</tr>"); //$NON-NLS-1$
658     }
659
660     protected String getRVIString(ReadGraph graph, Variable var) throws DatabaseException {
661         
662         try {
663             return var.getRVI(graph).toString(graph);
664         } catch (Throwable e) {
665             return "No RVI"; //$NON-NLS-1$
666         }
667         
668     }
669     
670     protected synchronized String calculateContent(final ReadGraph graph, String... uris) throws DatabaseException {
671         
672         L0 = Layer0.getInstance(graph);
673
674         StringBuilder content = new StringBuilder();
675
676         // Generate HTML -page
677         content.append("<html><head>").append(getHead()).append("</head>\n"); //$NON-NLS-1$ //$NON-NLS-2$
678         content.append("<body>\n"); //$NON-NLS-1$
679         content.append("<div id=\"mainContent\">\n"); //$NON-NLS-1$
680         for (String uri : uris) {
681             //System.out.println("URI: " + uri);
682             Variable var = Variables.getPossibleVariable(graph, uri);
683             if (var == null)
684                 continue;
685
686             String rviString = getRVIString(graph, var);
687             Object node = null;
688             if(var instanceof AbstractChildVariable) {
689                 VariableNode<?> vn = ((AbstractChildVariable)var).node; 
690                 if(vn != null) node = vn.node;
691             }
692             if(var instanceof AbstractPropertyVariable) {
693                 VariableNode<?> vn = ((AbstractPropertyVariable)var).node;
694                 if(vn != null) node = vn.node;
695             }
696             
697             // Begin #top DIV
698             content.append("<div id=\"top\">\n"); //$NON-NLS-1$
699             content.append("<table class=\"top\">\n"); //$NON-NLS-1$
700             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$
701             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$
702             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$
703             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$
704             content.append("</table>\n"); //$NON-NLS-1$
705             content.append("</div>\n"); //$NON-NLS-1$
706             // Close #top DIV
707
708             // Content
709             TreeMap<String, Variable> map = new TreeMap<String, Variable>();
710             try {
711                 for(Variable child : var.getChildren(graph)) {
712                     String name = getVariableName(graph, child);
713                     map.put(name, child);
714                 }
715             } catch (DatabaseException e) {
716                 // This may happen if the Variable implementation is broken
717                 ErrorLogger.defaultLogError("Broken variable child retrieval implementation or serious modelling error encountered. See exception for details.", e); //$NON-NLS-1$
718             }
719
720             TreeMap<String, Variable> map2 = new TreeMap<String, Variable>();
721             try {
722                 for(Variable child : var.getProperties(graph)) {
723                     String name = getVariableName(graph, child);
724                     map2.put(name, child);
725                 }
726             } catch (DatabaseException e) {
727                 // This may happen if the Variable implementation is broken
728                 ErrorLogger.defaultLogError("Broken variable property retrieval implementation or serious modelling error encountered. See exception for details.", e); //$NON-NLS-1$
729             }
730
731             content.append("\n<div id=\"data\">\n"); //$NON-NLS-1$
732             content.append("<table>\n"); //$NON-NLS-1$
733
734             content.append("<tr><th>Child</th></tr>"); //$NON-NLS-1$
735             for (Variable child : map.values()) {
736                 content.append("<tr><td>").append(getVariableRef(graph, child)).append("</td></tr>"); //$NON-NLS-1$ //$NON-NLS-2$
737             }
738
739             content.append("<tr><th>Property</th><th>Value</th><th>Datatype</th></tr>"); //$NON-NLS-1$
740             for (Variable property : map2.values()) {
741                 updateProperty(content, graph, property);
742             }
743             // Close #data
744             content.append("</div>\n\n"); //$NON-NLS-1$
745         }
746
747         // Close #mainContent
748         content.append("</div>\n"); //$NON-NLS-1$
749         content.append("</body></html>\n"); //$NON-NLS-1$
750
751         // Update content
752         return content.toString();
753     }
754
755     private String getHead() {
756         String result = ""; //$NON-NLS-1$
757         if (cssPath != null) {
758             result = "<link href=\"" + cssPath + "\" rel=\"stylesheet\" type=\"text/css\">"; //$NON-NLS-1$ //$NON-NLS-2$
759         }
760         return result;
761     }
762
763 }