1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.flightrecorder.ui.common;
  34 
  35 import java.awt.Color;
  36 import java.util.HashMap;
  37 import java.util.Iterator;
  38 import java.util.Map;
  39 import java.util.stream.Stream;
  40 
  41 import org.eclipse.osgi.util.NLS;
  42 import org.eclipse.swt.graphics.Image;
  43 import org.openjdk.jmc.common.IDisplayable;
  44 import org.openjdk.jmc.common.IMCStackTrace;
  45 import org.openjdk.jmc.common.item.IAttribute;
  46 import org.openjdk.jmc.common.item.IItem;
  47 import org.openjdk.jmc.common.item.IType;
  48 import org.openjdk.jmc.common.item.ItemToolkit;
  49 import org.openjdk.jmc.common.util.FormatToolkit;
  50 import org.openjdk.jmc.common.util.TypeHandling;
  51 import org.openjdk.jmc.common.util.XmlToolkit;
  52 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  53 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
  54 import org.openjdk.jmc.ui.charts.IChartInfoVisitor;
  55 import org.openjdk.jmc.ui.common.util.AdapterUtil;
  56 import org.openjdk.jmc.ui.misc.SWTColorToolkit;
  57 
  58 /**
  59  * Default chart tooltip provider. Note that each instance is only used once.
  60  */
  61 // FIXME: Rework to use StyledText instead, since FormText has overflow problems on HiDPI screens
  62 // (at least up to and including Eclipse 4.5). Also, use indentation for scopes and "At" sections.
  63 public class ChartToolTipProvider implements IChartInfoVisitor {
  64         private static final int MAXIMUM_VISIBLE_STACK_TRACE_ELEMENTS = 10;
  65         private static final String INDENT_4_NBSP = "    "; //$NON-NLS-1$
  66         private static final String INITIAL_HTML = "<form>"; //$NON-NLS-1$
  67 
  68         protected StringBuilder text = new StringBuilder(INITIAL_HTML);
  69         private final Map<String, Image> imageMap = new HashMap<>();
  70         private int colorNum;
  71         protected int bulletIndent;
  72         protected String lastAt;
  73 
  74         /**
  75          * Return the HTML. This method should typically only be called once. (Though technically, with
  76          * the current implementation, as long as it returns null, it could be called again. There's a
  77          * potential use case where this could be useful. If that is ever used, change this statement.)
  78          *
  79          * @return the HTML text or null if no tooltip should be shown.
  80          */
  81         public String getHTML() {
  82                 if (text.length() <= INITIAL_HTML.length()) {
  83                         return null;
  84                 }
  85                 text.append("</form>"); //$NON-NLS-1$
  86                 return text.toString();
  87         }
  88 
  89         public Map<String, Image> getImages() {
  90                 return imageMap;
  91         }
  92 
  93         protected Stream<IAttribute<?>> getAttributeStream(IType<IItem> type) {
  94                 return type.getAttributes().stream();
  95         }
  96 
  97         @Override
  98         public boolean enterScope(String context, boolean fullyShown) {
  99                 if (!fullyShown) {
 100                         text.append("<p>").append(htmlify(context)).append("</p>"); //$NON-NLS-1$ //$NON-NLS-2$
 101                         bulletIndent += 16;
 102                         return true;
 103                 }
 104                 return false;
 105         }
 106 
 107         @Override
 108         public void leaveScope() {
 109                 bulletIndent -= 16;
 110         }
 111 
 112         @Override
 113         public void hover(Object data) {
 114                 // Auto-generated method stub
 115         }
 116 
 117         protected String format(IDisplayable value) {
 118                 if (value != null) {
 119                         // FIXME: Add formatter that does AUTO (EXACT) or so.
 120                         String auto = value.displayUsing(IDisplayable.AUTO);
 121 //                      String exact = value.displayUsing(IDisplayable.EXACT);
 122 //                      return (auto.equals(exact)) ? htmlify(auto) : htmlify(auto + " (" + exact + ')');
 123                         return htmlify(auto);
 124                 } else {
 125                         return Messages.N_A;
 126                 }
 127         }
 128 
 129         protected void appendTagLI(Color color) {
 130                 if (color != null) {
 131                         imageMap.put("color." + colorNum, SWTColorToolkit.getColorThumbnail(SWTColorToolkit.asRGB(color))); //$NON-NLS-1$
 132                         text.append("<li style='image' value='color.").append(colorNum).append("' "); //$NON-NLS-1$ //$NON-NLS-2$
 133                         colorNum++;
 134                 } else {
 135                         text.append("<li "); //$NON-NLS-1$
 136                 }
 137 //              text.append("bindent='").append(bulletIndent).append("'> "); //$NON-NLS-1$ //$NON-NLS-2$
 138                 text.append("bindent='0'> "); //$NON-NLS-1$
 139         }
 140 
 141         protected void appendTitle(String title) {
 142                 text.append("<p><b>").append(title).append("</b></p>"); //$NON-NLS-1$ //$NON-NLS-2$
 143         }
 144 
 145         protected void appendAtIfNew(IDisplayable newAt) {
 146                 String newAtAsString = format(newAt);
 147                 if (!newAtAsString.equals(lastAt)) {
 148                         text.append("<p><span nowrap='true'>At ").append(newAtAsString).append(":<br/></span></p>"); //$NON-NLS-1$ //$NON-NLS-2$
 149                         lastAt = newAtAsString;
 150                 }
 151         }
 152 
 153         @Override
 154         public void visit(IPoint point) {
 155                 appendAtIfNew(point.getX());
 156                 appendTagLI(point.getColor());
 157                 String name = point.getName();
 158                 text.append("<span nowrap='true'>"); //$NON-NLS-1$
 159                 if (name != null) {
 160                         text.append(htmlify(name)).append(" = "); //$NON-NLS-1$
 161                 }
 162                 text.append(format(point.getY()));
 163                 text.append("</span></li>"); //$NON-NLS-1$
 164         }
 165 
 166         @Override
 167         public void visit(IBucket bucket) {
 168                 appendAtIfNew(bucket.getRange());
 169                 appendTagLI(bucket.getColor());
 170                 text.append("<span nowrap='true'>"); //$NON-NLS-1$
 171                 String name = bucket.getName();
 172                 if (name != null) {
 173                         text.append(htmlify(name));
 174                         text.append(" [").append(format(bucket.getWidth())).append("] = "); //$NON-NLS-1$ //$NON-NLS-2$
 175                 } else {
 176                         text.append("[").append(format(bucket.getWidth())).append("]: "); //$NON-NLS-1$ //$NON-NLS-2$
 177                 }
 178 
 179                 text.append(format(bucket.getY()));
 180                 text.append("</span></li>"); //$NON-NLS-1$
 181         }
 182 
 183         // FIXME: One idea was to let the user see the details in Properties/StackTrace views by click-selecting an event.
 184         @Override
 185         public void visit(ISpan span) {
 186                 if (span.getDescription() != null) {
 187                         appendTitle(span.getDescription());
 188                 }
 189                 appendAtIfNew(span.getRange());
 190                 appendTagLI(span.getColor());
 191                 // Would normally insert <span nowrap='true'> here, but since bold text is not displayed,
 192                 // it is inserted after the <b> element instead.
 193                 Object payload = span.getPayload();
 194                 IItem item = AdapterUtil.getAdapter(payload, IItem.class);
 195                 if (payload instanceof IDisplayable) {
 196                         text.append("<span nowrap='true'>"); //$NON-NLS-1$
 197                         text.append(format((IDisplayable) payload)).append(": "); //$NON-NLS-1$
 198                 } else if (item != null) {
 199                         IType<IItem> type = ItemToolkit.getItemType(item);
 200                         text.append("<b>").append(htmlify(type.getName())).append("</b><span nowrap='true'>: "); //$NON-NLS-1$ //$NON-NLS-2$
 201                 }
 202                 text.append(format(span.getWidth()));
 203                 text.append("</span></li>"); //$NON-NLS-1$
 204                 if (item != null) {
 205                         IType<IItem> type = ItemToolkit.getItemType(item);
 206                         IMCStackTrace trace = null;
 207                         Iterator<IAttribute<?>> attributes = getAttributeStream(type).iterator();
 208                         while (attributes.hasNext()) {
 209                                 IAttribute<?> attribute = attributes.next();
 210                                 if (attribute.equals(JfrAttributes.EVENT_STACKTRACE)) {
 211                                         trace = JfrAttributes.EVENT_STACKTRACE.getAccessor(type).getMember(item);
 212                                         continue;
 213                                 }
 214                                 text.append("<p vspace='false'><span nowrap='true'>"); //$NON-NLS-1$
 215                                 text.append(htmlify(attribute.getName())).append(": "); //$NON-NLS-1$
 216                                 // FIXME: Format timestamp with higher precision
 217                                 Object value = attribute.getAccessor(type).getMember(item);
 218                                 String valueString = TypeHandling.getValueString(value);
 219                                 text.append(htmlify(valueString));
 220                                 text.append("</span></p>"); //$NON-NLS-1$
 221                                 // Get value
 222                         }
 223                         if (trace != null) {
 224                                 text.append("<p vspace='false'/>"); //$NON-NLS-1$
 225                                 text.append("<p vspace='false'>"); //$NON-NLS-1$
 226                                 text.append(htmlify(JfrAttributes.EVENT_STACKTRACE.getName())).append(":<br/>"); //$NON-NLS-1$
 227                                 appendStackTrace(trace, true, false, true, true, true, false);
 228                                 text.append("</p>"); //$NON-NLS-1$
 229 
 230                         }
 231                 }
 232         }
 233 
 234         private void appendStackTrace(
 235                 IMCStackTrace trace, boolean showReturnValue, boolean showReturnValuePackage, boolean showClassName,
 236                 boolean showClassPackageName, boolean showArguments, boolean showArgumentsPackage) {
 237                 String indent = "    "; //$NON-NLS-1$
 238                 String lineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
 239                 String stackTraceString = FormatToolkit.getHumanReadable(trace, showReturnValue, showReturnValuePackage,
 240                                 showClassName, showClassPackageName, showArguments, showArgumentsPackage,
 241                                 MAXIMUM_VISIBLE_STACK_TRACE_ELEMENTS, indent, null, lineSeparator);
 242                 stackTraceString = htmlify(stackTraceString);
 243                 stackTraceString = stackTraceString.replace(indent, INDENT_4_NBSP);
 244                 stackTraceString = stackTraceString.replace(lineSeparator, "<br/>"); //$NON-NLS-1$
 245                 text.append(stackTraceString);
 246         }
 247 
 248         @Override
 249         public void visit(ITick tick) {
 250                 text.append("<p><span nowrap='true'>"); //$NON-NLS-1$
 251                 text.append(htmlify(tick.getValue().displayUsing(IDisplayable.VERBOSE)));
 252                 text.append("</span><br/></p>"); //$NON-NLS-1$
 253         }
 254 
 255         private static String htmlify(String text) {
 256                 return XmlToolkit.escapeTagContent(text);
 257         }
 258 
 259         @Override
 260         public void visit(ILane lane) {
 261                 text.append("<p><span nowrap='true'>"); //$NON-NLS-1$
 262                 text.append(htmlify(NLS.bind(Messages.ChartToolTipProvider_CAPTION_NAME, lane.getLaneName())));
 263                 text.append("</span><br/><span nowrap='true'>"); //$NON-NLS-1$
 264                 if (lane.getLaneDescription() != null && !lane.getLaneDescription().isEmpty()) {
 265                         text.append(
 266                                         htmlify(NLS.bind(Messages.ChartToolTipProvider_CAPTION_DESCRIPTION, lane.getLaneDescription())));
 267                 }
 268                 text.append("</span></p>"); //$NON-NLS-1$
 269         }
 270 }