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         protected String format(IDisplayable value) {
 113                 if (value != null) {
 114                         // FIXME: Add formatter that does AUTO (EXACT) or so.
 115                         String auto = value.displayUsing(IDisplayable.AUTO);
 116 //                      String exact = value.displayUsing(IDisplayable.EXACT);
 117 //                      return (auto.equals(exact)) ? htmlify(auto) : htmlify(auto + " (" + exact + ')');
 118                         return htmlify(auto);
 119                 } else {
 120                         return Messages.N_A;
 121                 }
 122         }
 123 
 124         protected void appendTagLI(Color color) {
 125                 if (color != null) {
 126                         imageMap.put("color." + colorNum, SWTColorToolkit.getColorThumbnail(SWTColorToolkit.asRGB(color))); //$NON-NLS-1$
 127                         text.append("<li style='image' value='color.").append(colorNum).append("' "); //$NON-NLS-1$ //$NON-NLS-2$
 128                         colorNum++;
 129                 } else {
 130                         text.append("<li "); //$NON-NLS-1$
 131                 }
 132 //              text.append("bindent='").append(bulletIndent).append("'> "); //$NON-NLS-1$ //$NON-NLS-2$
 133                 text.append("bindent='0'> "); //$NON-NLS-1$
 134         }
 135 
 136         protected void appendTitle(String title) {
 137                 text.append("<p><b>").append(title).append("</b></p>"); //$NON-NLS-1$ //$NON-NLS-2$
 138         }
 139 
 140         protected void appendAtIfNew(IDisplayable newAt) {
 141                 String newAtAsString = format(newAt);
 142                 if (!newAtAsString.equals(lastAt)) {
 143                         text.append("<p><span nowrap='true'>At ").append(newAtAsString).append(":<br/></span></p>"); //$NON-NLS-1$ //$NON-NLS-2$
 144                         lastAt = newAtAsString;
 145                 }
 146         }
 147 
 148         @Override
 149         public void visit(IPoint point) {
 150                 appendAtIfNew(point.getX());
 151                 appendTagLI(point.getColor());
 152                 String name = point.getName();
 153                 text.append("<span nowrap='true'>"); //$NON-NLS-1$
 154                 if (name != null) {
 155                         text.append(htmlify(name)).append(" = "); //$NON-NLS-1$
 156                 }
 157                 text.append(format(point.getY()));
 158                 text.append("</span></li>"); //$NON-NLS-1$
 159         }
 160 
 161         @Override
 162         public void visit(IBucket bucket) {
 163                 appendAtIfNew(bucket.getRange());
 164                 appendTagLI(bucket.getColor());
 165                 text.append("<span nowrap='true'>"); //$NON-NLS-1$
 166                 String name = bucket.getName();
 167                 if (name != null) {
 168                         text.append(htmlify(name));
 169                         text.append(" [").append(format(bucket.getWidth())).append("] = "); //$NON-NLS-1$ //$NON-NLS-2$
 170                 } else {
 171                         text.append("[").append(format(bucket.getWidth())).append("]: "); //$NON-NLS-1$ //$NON-NLS-2$
 172                 }
 173 
 174                 text.append(format(bucket.getY()));
 175                 text.append("</span></li>"); //$NON-NLS-1$
 176         }
 177 
 178         // FIXME: One idea was to let the user see the details in Properties/StackTrace views by click-selecting an event.
 179         @Override
 180         public void visit(ISpan span) {
 181                 if (span.getDescription() != null) {
 182                         appendTitle(span.getDescription());
 183                 }
 184                 appendAtIfNew(span.getRange());
 185                 appendTagLI(span.getColor());
 186                 // Would normally insert <span nowrap='true'> here, but since bold text is not displayed,
 187                 // it is inserted after the <b> element instead.
 188                 Object payload = span.getPayload();
 189                 IItem item = AdapterUtil.getAdapter(payload, IItem.class);
 190                 if (payload instanceof IDisplayable) {
 191                         text.append("<span nowrap='true'>"); //$NON-NLS-1$
 192                         text.append(format((IDisplayable) payload)).append(": "); //$NON-NLS-1$
 193                 } else if (item != null) {
 194                         IType<IItem> type = ItemToolkit.getItemType(item);
 195                         text.append("<b>").append(htmlify(type.getName())).append("</b><span nowrap='true'>: "); //$NON-NLS-1$ //$NON-NLS-2$
 196                 }
 197                 text.append(format(span.getWidth()));
 198                 text.append("</span></li>"); //$NON-NLS-1$
 199                 if (item != null) {
 200                         IType<IItem> type = ItemToolkit.getItemType(item);
 201                         IMCStackTrace trace = null;
 202                         Iterator<IAttribute<?>> attributes = getAttributeStream(type).iterator();
 203                         while (attributes.hasNext()) {
 204                                 IAttribute<?> attribute = attributes.next();
 205                                 if (attribute.equals(JfrAttributes.EVENT_STACKTRACE)) {
 206                                         trace = JfrAttributes.EVENT_STACKTRACE.getAccessor(type).getMember(item);
 207                                         continue;
 208                                 }
 209                                 text.append("<p vspace='false'><span nowrap='true'>"); //$NON-NLS-1$
 210                                 text.append(htmlify(attribute.getName())).append(": "); //$NON-NLS-1$
 211                                 // FIXME: Format timestamp with higher precision
 212                                 Object value = attribute.getAccessor(type).getMember(item);
 213                                 String valueString = TypeHandling.getValueString(value);
 214                                 text.append(htmlify(valueString));
 215                                 text.append("</span></p>"); //$NON-NLS-1$
 216                                 // Get value
 217                         }
 218                         if (trace != null) {
 219                                 text.append("<p vspace='false'/>"); //$NON-NLS-1$
 220                                 text.append("<p vspace='false'>"); //$NON-NLS-1$
 221                                 text.append(htmlify(JfrAttributes.EVENT_STACKTRACE.getName())).append(":<br/>"); //$NON-NLS-1$
 222                                 appendStackTrace(trace, true, false, true, true, true, false);
 223                                 text.append("</p>"); //$NON-NLS-1$
 224 
 225                         }
 226                 }
 227         }
 228 
 229         private void appendStackTrace(
 230                 IMCStackTrace trace, boolean showReturnValue, boolean showReturnValuePackage, boolean showClassName,
 231                 boolean showClassPackageName, boolean showArguments, boolean showArgumentsPackage) {
 232                 String indent = "    "; //$NON-NLS-1$
 233                 String lineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
 234                 String stackTraceString = FormatToolkit.getHumanReadable(trace, showReturnValue, showReturnValuePackage,
 235                                 showClassName, showClassPackageName, showArguments, showArgumentsPackage,
 236                                 MAXIMUM_VISIBLE_STACK_TRACE_ELEMENTS, indent, null, lineSeparator);
 237                 stackTraceString = htmlify(stackTraceString);
 238                 stackTraceString = stackTraceString.replace(indent, INDENT_4_NBSP);
 239                 stackTraceString = stackTraceString.replace(lineSeparator, "<br/>"); //$NON-NLS-1$
 240                 text.append(stackTraceString);
 241         }
 242 
 243         @Override
 244         public void visit(ITick tick) {
 245                 text.append("<p><span nowrap='true'>"); //$NON-NLS-1$
 246                 text.append(htmlify(tick.getValue().displayUsing(IDisplayable.VERBOSE)));
 247                 text.append("</span><br/></p>"); //$NON-NLS-1$
 248         }
 249 
 250         private static String htmlify(String text) {
 251                 return XmlToolkit.escapeTagContent(text);
 252         }
 253 
 254         @Override
 255         public void visit(ILane lane) {
 256                 text.append("<p><span nowrap='true'>"); //$NON-NLS-1$
 257                 text.append(htmlify(NLS.bind(Messages.ChartToolTipProvider_CAPTION_NAME, lane.getLaneName())));
 258                 text.append("</span><br/><span nowrap='true'>"); //$NON-NLS-1$
 259                 if (lane.getLaneDescription() != null && !lane.getLaneDescription().isEmpty()) {
 260                         text.append(
 261                                         htmlify(NLS.bind(Messages.ChartToolTipProvider_CAPTION_DESCRIPTION, lane.getLaneDescription())));
 262                 }
 263                 text.append("</span></p>"); //$NON-NLS-1$
 264         }
 265 }