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.io.FileInputStream;
  37 import java.io.InputStream;
  38 import java.util.ArrayList;
  39 import java.util.Arrays;
  40 import java.util.Collection;
  41 import java.util.Collections;
  42 import java.util.Comparator;
  43 import java.util.HashMap;
  44 import java.util.HashSet;
  45 import java.util.Iterator;
  46 import java.util.List;
  47 import java.util.Map;
  48 import java.util.Objects;
  49 import java.util.Optional;
  50 import java.util.Set;
  51 import java.util.function.Consumer;
  52 import java.util.function.Predicate;
  53 import java.util.function.Supplier;
  54 import java.util.regex.Matcher;
  55 import java.util.regex.Pattern;
  56 import java.util.stream.Collectors;
  57 import java.util.stream.Stream;
  58 
  59 import org.eclipse.core.runtime.IAdaptable;
  60 import org.eclipse.jface.action.Action;
  61 import org.eclipse.jface.action.IAction;
  62 import org.eclipse.jface.action.IMenuListener;
  63 import org.eclipse.jface.action.IMenuManager;
  64 import org.eclipse.jface.action.MenuManager;
  65 import org.eclipse.jface.action.Separator;
  66 import org.eclipse.jface.dialogs.InputDialog;
  67 import org.eclipse.jface.resource.ImageDescriptor;
  68 import org.eclipse.jface.resource.JFaceResources;
  69 import org.eclipse.jface.viewers.ColumnViewer;
  70 import org.eclipse.jface.viewers.IBaseLabelProvider;
  71 import org.eclipse.jface.viewers.StyledCellLabelProvider;
  72 import org.eclipse.jface.viewers.ViewerCell;
  73 import org.eclipse.jface.window.ToolTip;
  74 import org.eclipse.jface.window.Window;
  75 import org.eclipse.jface.wizard.Wizard;
  76 import org.eclipse.jface.wizard.WizardDialog;
  77 import org.eclipse.jface.wizard.WizardPage;
  78 import org.eclipse.swt.SWT;
  79 import org.eclipse.swt.custom.CTabFolder;
  80 import org.eclipse.swt.custom.CTabItem;
  81 import org.eclipse.swt.events.SelectionAdapter;
  82 import org.eclipse.swt.events.SelectionEvent;
  83 import org.eclipse.swt.graphics.GC;
  84 import org.eclipse.swt.graphics.Image;
  85 import org.eclipse.swt.graphics.ImageData;
  86 import org.eclipse.swt.layout.FillLayout;
  87 import org.eclipse.swt.layout.GridData;
  88 import org.eclipse.swt.layout.GridLayout;
  89 import org.eclipse.swt.widgets.Button;
  90 import org.eclipse.swt.widgets.Composite;
  91 import org.eclipse.swt.widgets.Control;
  92 import org.eclipse.swt.widgets.Display;
  93 import org.eclipse.swt.widgets.Event;
  94 import org.eclipse.swt.widgets.FileDialog;
  95 import org.eclipse.swt.widgets.Label;
  96 import org.eclipse.ui.forms.widgets.Form;
  97 import org.eclipse.ui.forms.widgets.FormText;
  98 import org.eclipse.ui.forms.widgets.FormToolkit;
  99 import org.openjdk.jmc.common.item.IAggregator;
 100 import org.openjdk.jmc.common.item.IAttribute;
 101 import org.openjdk.jmc.common.item.ICanonicalAccessorFactory;
 102 import org.openjdk.jmc.common.item.IItem;
 103 import org.openjdk.jmc.common.item.IItemCollection;
 104 import org.openjdk.jmc.common.item.IItemFilter;
 105 import org.openjdk.jmc.common.item.IItemIterable;
 106 import org.openjdk.jmc.common.item.IItemQuery;
 107 import org.openjdk.jmc.common.item.IMemberAccessor;
 108 import org.openjdk.jmc.common.item.IType;
 109 import org.openjdk.jmc.common.item.ItemFilters;
 110 import org.openjdk.jmc.common.item.ItemToolkit;
 111 import org.openjdk.jmc.common.unit.IQuantity;
 112 import org.openjdk.jmc.common.unit.IRange;
 113 import org.openjdk.jmc.common.unit.KindOfQuantity;
 114 import org.openjdk.jmc.common.unit.QuantityRange;
 115 import org.openjdk.jmc.common.unit.RangeContentType;
 116 import org.openjdk.jmc.common.unit.UnitLookup;
 117 import org.openjdk.jmc.common.util.ColorToolkit;
 118 import org.openjdk.jmc.common.util.CompositeKey;
 119 import org.openjdk.jmc.flightrecorder.JfrAttributes;
 120 import org.openjdk.jmc.flightrecorder.jdk.JdkAttributes;
 121 import org.openjdk.jmc.flightrecorder.jdk.JdkFilters;
 122 import org.openjdk.jmc.flightrecorder.jdk.JdkTypeIDs;
 123 import org.openjdk.jmc.flightrecorder.rules.Result;
 124 import org.openjdk.jmc.flightrecorder.rules.Severity;
 125 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
 126 import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
 127 import org.openjdk.jmc.flightrecorder.ui.ItemCollectionToolkit;
 128 import org.openjdk.jmc.flightrecorder.ui.ItemIterableToolkit;
 129 import org.openjdk.jmc.flightrecorder.ui.PageManager;
 130 import org.openjdk.jmc.flightrecorder.ui.common.ItemHistogram.CompositeKeyHistogramBuilder;
 131 import org.openjdk.jmc.flightrecorder.ui.common.ItemList.ItemListBuilder;
 132 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
 133 import org.openjdk.jmc.flightrecorder.ui.overview.ResultOverview;
 134 import org.openjdk.jmc.flightrecorder.ui.pages.itemhandler.ItemHandlerPage;
 135 import org.openjdk.jmc.flightrecorder.ui.pages.itemhandler.ItemHandlerPage.ItemHandlerUiStandIn;
 136 import org.openjdk.jmc.flightrecorder.ui.selection.IFilterFlavor;
 137 import org.openjdk.jmc.flightrecorder.ui.selection.SelectionStore.SelectionStoreEntry;
 138 import org.openjdk.jmc.flightrecorder.ui.selection.SelectionStoreActionToolkit;
 139 import org.openjdk.jmc.ui.charts.AWTChartToolkit.IColorProvider;
 140 import org.openjdk.jmc.ui.charts.IQuantitySeries;
 141 import org.openjdk.jmc.ui.charts.ISpanSeries;
 142 import org.openjdk.jmc.ui.charts.IXDataRenderer;
 143 import org.openjdk.jmc.ui.charts.QuantitySeries;
 144 import org.openjdk.jmc.ui.charts.RendererToolkit;
 145 import org.openjdk.jmc.ui.charts.SpanRenderer;
 146 import org.openjdk.jmc.ui.charts.SubdividedQuantityRange;
 147 import org.openjdk.jmc.ui.charts.XYChart;
 148 import org.openjdk.jmc.ui.charts.XYDataRenderer;
 149 import org.openjdk.jmc.ui.charts.XYQuantities;
 150 import org.openjdk.jmc.ui.column.ColumnMenusFactory;
 151 import org.openjdk.jmc.ui.column.TableSettings;
 152 import org.openjdk.jmc.ui.column.TableSettings.ColumnSettings;
 153 import org.openjdk.jmc.ui.handlers.ActionToolkit;
 154 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
 155 import org.openjdk.jmc.ui.misc.ChartCanvas;
 156 import org.openjdk.jmc.ui.misc.CompositeToolkit;
 157 import org.openjdk.jmc.ui.misc.DisplayToolkit;
 158 import org.openjdk.jmc.ui.misc.FilterEditor;
 159 import org.openjdk.jmc.ui.misc.FilterEditor.AttributeValueProvider;
 160 import org.openjdk.jmc.ui.misc.OverlayImageDescriptor;
 161 import org.openjdk.jmc.ui.misc.SWTColorToolkit;
 162 
 163 public class DataPageToolkit {
 164 
 165         public static final IColorProvider<IItem> ITEM_COLOR = item -> TypeLabelProvider
 166                         .getColorOrDefault(item.getType().getIdentifier());
 167 
 168         public static final IColorProvider<IItem> getAttributeValueColor(final IAttribute<?> attribute) {
 169                 return new IColorProvider<IItem>() {
 170 
 171                         @Override
 172                         public Color getColor(IItem item) {
 173                                 IMemberAccessor<?, IItem> accessor = attribute.getAccessor(ItemToolkit.getItemType(item));
 174                                 Object attributeValue = accessor != null ? accessor.getMember(item) : null;
 175                                 // FIXME: Should we include the type or not?
 176                                 return attributeValue != null
 177                                                 ? TypeLabelProvider.getColorOrDefault(attribute.getIdentifier() + "=" + attributeValue + "(" //$NON-NLS-1$ //$NON-NLS-2$
 178                                                                 + item.getType().getIdentifier() + ")") //$NON-NLS-1$
 179                                                 : ITEM_COLOR.getColor(item);
 180                         }
 181                 };
 182         }
 183 
 184         private static final Map<String, Color> FIELD_COLOR_MAP = new HashMap<>();
 185         private static final Map<String, Integer> DEFAULT_COLUMNS_ORDER;
 186 
 187         static {
 188 
 189                 // FIXME: Create FieldAppearance class, similar to TypeAppearence?
 190                 FIELD_COLOR_MAP.put(JdkAttributes.MACHINE_TOTAL.getIdentifier(), new Color(255, 128, 0));
 191                 FIELD_COLOR_MAP.put(JdkAttributes.JVM_SYSTEM.getIdentifier(), new Color(128, 128, 128));
 192                 FIELD_COLOR_MAP.put(JdkAttributes.JVM_USER.getIdentifier(), new Color(0, 0, 255));
 193                 FIELD_COLOR_MAP.put(JdkAttributes.JVM_TOTAL.getIdentifier(), new Color(64, 64, 191));
 194 
 195                 // FIXME: Handle ColorProvider and combined events
 196                 Map<String, Integer> columnsOrderMap = new HashMap<>();
 197                 columnsOrderMap.put(createColumnId(JfrAttributes.START_TIME), 1);
 198                 columnsOrderMap.put(createColumnId(JfrAttributes.DURATION), 2);
 199                 columnsOrderMap.put(createColumnId(JfrAttributes.END_TIME), 3);
 200                 columnsOrderMap.put(createColumnId(JfrAttributes.EVENT_THREAD), 4);
 201                 DEFAULT_COLUMNS_ORDER = Collections.unmodifiableMap(columnsOrderMap);
 202         }
 203 
 204         public static final Color ALLOCATION_COLOR = new Color(64, 144, 230);
 205 
 206         public static final String FORM_TOOLBAR_PAGE_RESULTS = "pageResults"; //$NON-NLS-1$
 207         public static final String FORM_TOOLBAR_PAGE_SETUP = "pageSetup"; //$NON-NLS-1$
 208         public static final String FORM_TOOLBAR_PAGE_NAV = "pageNav"; //$NON-NLS-1$
 209 
 210         public static final String RESULT_ACTION_ID = "resultAction"; //$NON-NLS-1$
 211 
 212         public static Color getFieldColor(String fieldId) {
 213                 return FIELD_COLOR_MAP.getOrDefault(fieldId, ColorToolkit.getDistinguishableColor(fieldId));
 214         }
 215 
 216         public static Color getFieldColor(IAttribute<?> attribute) {
 217                 return getFieldColor(attribute.getIdentifier());
 218         }
 219 
 220         public static TableSettings createTableSettingsByOrderByAndColumnsWithDefaultOrdering(
 221                 final String orderBy, final Collection<ColumnSettings> columns) {
 222                 final Stream<ColumnSettings> defaultOrderColumns = columns.stream()
 223                                 .filter(c -> DEFAULT_COLUMNS_ORDER.containsKey(c.getId())).filter(c -> !c.isHidden())
 224                                 .sorted((c1, c2) -> Integer.compare(DEFAULT_COLUMNS_ORDER.get(c1.getId()),
 225                                                 DEFAULT_COLUMNS_ORDER.get(c2.getId())));
 226                 final Stream<ColumnSettings> naturalOrderColumns = columns.stream()
 227                                 .filter(c -> !DEFAULT_COLUMNS_ORDER.containsKey(c.getId()))
 228                                 .sorted((c1, c2) -> String.CASE_INSENSITIVE_ORDER.compare(c1.getId(), c2.getId()));
 229                 final List<ColumnSettings> resultColumns = Stream.concat(defaultOrderColumns, naturalOrderColumns)
 230                                 .collect(Collectors.toList());
 231                 return new TableSettings(orderBy, resultColumns);
 232         }
 233 
 234         public static TableSettings createTableSettingsByAllAndVisibleColumns(
 235                 final Collection<String> allColumns, final Collection<String> visibleColumns) {
 236                 final List<ColumnSettings> defaultListCols = new ArrayList<>();
 237                 for (String columnId : allColumns) {
 238                         defaultListCols.add(new ColumnSettings(columnId, !visibleColumns.contains(columnId), null, null));
 239                 }
 240                 return createTableSettingsByOrderByAndColumnsWithDefaultOrdering(null, defaultListCols);
 241         }
 242 
 243         private static String createColumnId(IAttribute<?> attr) {
 244                 return new StringBuilder().append(attr.getIdentifier()).append(":") //$NON-NLS-1$
 245                                 .append(attr.getContentType().getIdentifier()).toString();
 246         }
 247 
 248         public static IAction createAttributeCheckAction(IAttribute<?> attribute, Consumer<Boolean> onChange) {
 249                 return createCheckAction(attribute.getName(), attribute.getDescription(), attribute.getIdentifier(),
 250                                 getFieldColor(attribute), onChange);
 251         }
 252 
 253         public static IAction createTypeCheckAction(
 254                 String actionId, String typeId, String name, String description, Consumer<Boolean> onChange) {
 255                 return createCheckAction(name, description, actionId, TypeLabelProvider.getColorOrDefault(typeId), onChange);
 256         }
 257 
 258         public static IAction createAggregatorCheckAction(
 259                 IAggregator<?, ?> aggregator, String id, Color color, Consumer<Boolean> onChange) {
 260                 return createCheckAction(aggregator.getName(), aggregator.getDescription(), id, color, onChange);
 261         }
 262 
 263         public static IAction createCheckAction(
 264                 String name, String description, String id, Color color, Consumer<Boolean> onChange) {
 265                 return createCheckAction(name, description, id,
 266                                 SWTColorToolkit.getColorThumbnailDescriptor(SWTColorToolkit.asRGB(color)), onChange);
 267         }
 268 
 269         public static IAction createCheckAction(
 270                 String name, String description, String id, ImageDescriptor icon, Consumer<Boolean> onChange) {
 271                 return ActionToolkit.checkAction(onChange, name, description, icon, id);
 272         }
 273 
 274         public static Optional<IXDataRenderer> buildLinesRow(
 275                 String title, String description, IItemCollection items, boolean fill, IItemQuery query,
 276                 Predicate<IAttribute<IQuantity>> attributeFilter, IQuantity includeLow, IQuantity includeHigh) {
 277                 XYDataRenderer renderer = includeHigh != null
 278                                 ? new XYDataRenderer(includeLow, includeHigh, true, title, description)
 279                                 : new XYDataRenderer(includeLow, title, description);
 280                 IItemCollection filteredItemsSupplier = items.apply(query.getFilter());
 281                 Stream<IAttribute<IQuantity>> attributes = getQuantityAttributes(query);
 282                 if (attributeFilter != null) {
 283                         attributes = attributes.filter(attributeFilter);
 284                 }
 285                 if (DataPageToolkit.addEndTimeLines(renderer, filteredItemsSupplier, fill, attributes)) {
 286                         return Optional.of(new ItemRow(title, description, renderer, filteredItemsSupplier));
 287                 }
 288                 return Optional.empty();
 289         }
 290 
 291         /**
 292          * @param q
 293          *            A query containing only {@code IAttribute<IQuantity>} attributes. Queries
 294          *            containing non-quantity attributes are not supported and may cause
 295          *            ClassCastExceptions later when the attributes are used.
 296          * @return a stream of the query attributes
 297          */
 298         /*
 299          * FIXME: JMC-5125 - This cast chain is scary and should be reworked.
 300          * 
 301          * If the query contains any non-quantity attributes then there will be a ClassCastException
 302          * later when the attributes are used to extract values.
 303          */
 304         @SuppressWarnings({"unchecked", "rawtypes"})
 305         public static Stream<IAttribute<IQuantity>> getQuantityAttributes(IItemQuery q) {
 306                 return (Stream) q.getAttributes().stream();
 307         }
 308 
 309         public static void setChart(ChartCanvas canvas, XYChart chart, Consumer<IItemCollection> selectionListener) {
 310                 setChart(canvas, chart, selectionListener, null);
 311         }
 312 
 313         public static void setChart(
 314                 ChartCanvas canvas, XYChart chart, Consumer<IItemCollection> selectionListener,
 315                 Consumer<IRange<IQuantity>> selectRangeConsumer) {
 316                 IMenuManager contextMenu = canvas.getContextMenu();
 317                 contextMenu.removeAll();
 318                 canvas.getContextMenu().add(new Action(Messages.CHART_ZOOM_TO_SELECTED_RANGE) {
 319                         @Override
 320                         public void run() {
 321                                 IQuantity selectionStart = chart.getSelectionStart();
 322                                 IQuantity selectionEnd = chart.getSelectionEnd();
 323                                 if (selectionStart == null || selectionEnd == null) {
 324                                         chart.clearVisibleRange();
 325                                 } else {
 326                                         chart.setVisibleRange(selectionStart, selectionEnd);
 327                                 }
 328                                 canvas.redrawChart();
 329                         }
 330                 });
 331 
 332                 canvas.setSelectionListener(() -> {
 333                         selectionListener.accept(ItemRow.getRangeSelection(chart, JfrAttributes.LIFETIME));
 334                         IQuantity start = chart.getSelectionStart();
 335                         IQuantity end = chart.getSelectionEnd();
 336                         if (selectRangeConsumer != null) {
 337                                 selectRangeConsumer
 338                                                 .accept(start != null && end != null ? QuantityRange.createWithEnd(start, end) : null);
 339                         }
 340                 });
 341                 canvas.setChart(chart);
 342         }
 343 
 344         public static void setChart(
 345                 ChartCanvas canvas, XYChart chart, IAttribute<IQuantity> selectionAttribute,
 346                 Consumer<IItemCollection> selectionListener) {
 347                 IMenuManager contextMenu = canvas.getContextMenu();
 348                 contextMenu.removeAll();
 349                 canvas.setSelectionListener(() -> selectionListener.accept(ItemRow.getSelection(chart, selectionAttribute)));
 350                 canvas.setChart(chart);
 351         }
 352 
 353         /**
 354          * Only works for items that are either fully overlapping, or disjunct. Must be ensured by
 355          * client code.
 356          */
 357         private static class RangePayload implements IAdaptable {
 358                 IItem item;
 359                 IQuantity start;
 360                 IQuantity end;
 361                 double rangeInPixels;
 362 
 363                 RangePayload(IItem item, IQuantity start, IQuantity end, double rangeInPixels) {
 364                         this.item = item;
 365                         this.start = start;
 366                         this.end = end;
 367                         this.rangeInPixels = rangeInPixels;
 368                 }
 369 
 370                 void combineWith(IItem item, IQuantity start, IQuantity end, double rangeInPixels) {
 371                         if (this.start.compareTo(start) < 0) {
 372                                 // Will choose the item that starts last
 373                                 this.start = start;
 374                                 this.end = end;
 375                                 this.item = item;
 376                                 extendRangeInPixels(this.end.compareTo(start) > 0, rangeInPixels);
 377                         } else {
 378                                 extendRangeInPixels(end.compareTo(this.start) > 0, rangeInPixels);
 379                         }
 380                 }
 381 
 382                 void extendRangeInPixels(boolean overlapping, double rangeInPixels) {
 383                         this.rangeInPixels = overlapping ? Math.max(this.rangeInPixels, rangeInPixels)
 384                                         : this.rangeInPixels + rangeInPixels;
 385                 }
 386 
 387                 @Override
 388                 public <T> T getAdapter(Class<T> adapter) {
 389                         return IItem.class.isAssignableFrom(adapter) ? adapter.cast(item) : null;
 390                 }
 391         }
 392 
 393         private static ISpanSeries<RangePayload> rangeSeries(
 394                 IItemCollection events, IAttribute<IQuantity> startAttribute, IAttribute<IQuantity> endAttribute) {
 395                 return new ISpanSeries<RangePayload>() {
 396                         @Override
 397                         public XYQuantities<RangePayload[]> getQuantities(SubdividedQuantityRange xBucketRange) {
 398                                 SubdividedQuantityRange xRange = xBucketRange.copyWithPixelSubdividers();
 399                                 List<RangePayload> spanningPixels = new ArrayList<>();
 400                                 RangePayload[] pixelBuckets = new RangePayload[xRange.getNumSubdividers()];
 401                                 events.forEach(is -> {
 402                                         IMemberAccessor<IQuantity, IItem> startAccessor = startAttribute.getAccessor(is.getType());
 403                                         IMemberAccessor<IQuantity, IItem> endAccessor = endAttribute.getAccessor(is.getType());
 404                                         is.forEach(item -> {
 405                                                 IQuantity start = startAccessor.getMember(item);
 406                                                 IQuantity end = endAccessor.getMember(item);
 407                                                 int xPos = xRange.getFloorSubdivider(start);
 408                                                 int endPos = xRange.getFloorSubdivider(end);
 409                                                 if (xPos < pixelBuckets.length && endPos >= 0) {
 410                                                         // FIXME: If we have very short events (nanosecond scale) we can sometimes get a negative range.
 411                                                         double rangeInPixels = xRange.getPixel(end) - xRange.getPixel(start);
 412                                                         if (xPos != endPos) {
 413                                                                 spanningPixels.add(new RangePayload(item, start, end, rangeInPixels));
 414                                                         } else if (pixelBuckets[xPos] == null) {
 415                                                                 pixelBuckets[xPos] = new RangePayload(item, start, end, rangeInPixels);
 416                                                         } else {
 417                                                                 pixelBuckets[xPos].combineWith(item, start, end, rangeInPixels);
 418                                                         }
 419                                                 }
 420                                         });
 421                                 });
 422                                 RangePayload[] sorted = Stream
 423                                                 .concat(Stream.of(pixelBuckets).filter(Objects::nonNull), spanningPixels.stream())
 424                                                 .sorted(Comparator.comparing(r -> r.start)).toArray(RangePayload[]::new);
 425                                 // FIXME: Should make it possible to use the RangePayload[] directly instead
 426                                 List<IQuantity> starts = Stream.of(sorted).map(r -> r.start).collect(Collectors.toList());
 427                                 List<IQuantity> ends = Stream.of(sorted).map(r -> r.end).collect(Collectors.toList());
 428                                 return XYQuantities.create(sorted, starts, ends, xRange);
 429                         }
 430 
 431                         @Override
 432                         public IQuantity getStartX(RangePayload payload) {
 433                                 return payload.start;
 434                         }
 435                 };
 436         }
 437 
 438         public final static Color GC_BASE_COLOR = TypeLabelProvider.getColor(JdkTypeIDs.GC_PAUSE);
 439         private final static Color VM_OPERATIONS_BASE_COLOR = TypeLabelProvider.getColor(JdkTypeIDs.VM_OPERATIONS);
 440         private final static IColorProvider<RangePayload> GC_COLOR = payload -> adjustAlpha(GC_BASE_COLOR,
 441                         payload.rangeInPixels);
 442         private final static IColorProvider<RangePayload> APPLICATION_PAUSE_COLOR = payload -> adjustAlpha(
 443                         payload.item.getType().getIdentifier().equals(JdkTypeIDs.GC_PAUSE) ? GC_BASE_COLOR
 444                                         : VM_OPERATIONS_BASE_COLOR,
 445                         payload.rangeInPixels);
 446         public final static ImageDescriptor GC_LEGEND_ICON = new OverlayImageDescriptor(
 447                         SWTColorToolkit.getColorThumbnailDescriptor(SWTColorToolkit.asRGB(GC_BASE_COLOR)), false,
 448                         FlightRecorderUI.getDefault().getMCImageDescriptor("trash_overlay.png")); //$NON-NLS-1$
 449 
 450         /**
 451          * Return a color with alpha calculated from a fraction.
 452          *
 453          * @param color
 454          *            a base color
 455          * @param fraction
 456          *            A value where 0 gives the lowest alpha value and 1 gives the highest. Fractions
 457          *            above 1 are accepted and treated as 1. Negative fractions should not be used.
 458          * @return a color with RGB from the base color and an alpha value depending on the fraction
 459          */
 460         private static Color adjustAlpha(Color color, double fraction) {
 461                 return ColorToolkit.withAlpha(color, Math.min(200, (int) ((Math.max(0, fraction) + 0.15) * 255)));
 462         }
 463 
 464         public static ItemRow buildGcPauseRow(IItemCollection items) {
 465                 IItemCollection pauseEvents = items.apply(JdkFilters.GC_PAUSE);
 466                 ISpanSeries<RangePayload> gcBackdrop = rangeSeries(pauseEvents, JfrAttributes.START_TIME,
 467                                 JfrAttributes.END_TIME);
 468                 return new ItemRow(SpanRenderer.build(gcBackdrop, GC_COLOR), pauseEvents);
 469         }
 470 
 471         public static ItemRow buildApplicationPauseRow(IItemCollection items) {
 472                 IItemFilter vmOperationPauseFilter = ItemFilters.and(JdkFilters.VM_OPERATIONS,
 473                                 ItemFilters.equals(JdkAttributes.SAFEPOINT, true));
 474                 IItemCollection applicationPauses = items
 475                                 .apply(ItemFilters.or(JdkFilters.GC_PAUSE, JdkFilters.SAFE_POINTS, vmOperationPauseFilter));
 476                 ISpanSeries<RangePayload> pausesSeries = rangeSeries(applicationPauses, JfrAttributes.START_TIME,
 477                                 JfrAttributes.END_TIME);
 478                 return new ItemRow(SpanRenderer.build(pausesSeries, APPLICATION_PAUSE_COLOR), applicationPauses);
 479         }
 480 
 481         public static IXDataRenderer buildTimestampHistogramRenderer(
 482                 IItemCollection items, IAggregator<IQuantity, ?> aggregator, IAttribute<IQuantity> timestampAttribute,
 483                 Color color) {
 484                 IQuantitySeries<IQuantity[]> aggregatorSeries = BucketBuilder.aggregatorSeries(items, aggregator,
 485                                 timestampAttribute);
 486                 XYDataRenderer renderer = new XYDataRenderer(getKindOfQuantity(aggregator).getDefaultUnit().quantity(0),
 487                                 aggregator.getName(), aggregator.getDescription());
 488                 renderer.addBarChart(aggregator.getName(), aggregatorSeries, color);
 489                 return renderer;
 490         }
 491 
 492         public static IXDataRenderer buildTimestampHistogramRenderer(
 493                 IItemCollection items, IAggregator<IQuantity, ?> aggregator, Color color) {
 494                 return buildTimestampHistogramRenderer(items, aggregator, JfrAttributes.CENTER_TIME, color);
 495         }
 496 
 497         public static ItemRow buildTimestampHistogram(
 498                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> aggregator,
 499                 IAttribute<IQuantity> timestampAttribute, Color color) {
 500                 return new ItemRow(title, description,
 501                                 buildTimestampHistogramRenderer(items, aggregator, timestampAttribute, color), items);
 502         }
 503 
 504         public static ItemRow buildTimestampHistogram(
 505                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> aggregator, Color color) {
 506                 return new ItemRow(title, description, buildTimestampHistogramRenderer(items, aggregator, color), items);
 507         }
 508 
 509         public static ItemHistogram createDistinctItemsTable(
 510                 Composite parent, IItemCollection items, IItemQuery query, TableSettings settings) {
 511                 CompositeKeyHistogramBuilder histogramBuilder = new CompositeKeyHistogramBuilder();
 512                 for (IAttribute<?> attribute : query.getAttributes()) {
 513                         histogramBuilder.addKeyColumn(attribute);
 514                 }
 515                 ItemHistogram table = histogramBuilder.buildWithoutBorder(parent, settings);
 516                 return table;
 517         }
 518 
 519         public static IBaseLabelProvider createTableHighlightProvider(Pattern highlightPattern, boolean isWarning) {
 520                 return new StyledCellLabelProvider() {
 521                         @Override
 522                         public void update(ViewerCell cell) {
 523                                 org.eclipse.swt.graphics.Color color = isWarning
 524                                                 ? new org.eclipse.swt.graphics.Color(Display.getCurrent(), 240, 120, 140)
 525                                                 : new org.eclipse.swt.graphics.Color(Display.getCurrent(), 255, 144, 4);
 526                                 String text = getText(cell.getElement(), cell.getColumnIndex());
 527                                 Matcher matcher = highlightPattern.matcher(text);
 528                                 if (matcher.find()) {
 529                                         cell.getViewerRow().setBackground(0, color);
 530                                         cell.getViewerRow().setBackground(1, color);
 531                                 }
 532                                 cell.setText(text);
 533                                 super.update(cell);
 534                         }
 535 
 536                         private String getText(Object element, int index) {
 537                                 Object key = AggregationGrid.getKey(element);
 538                                 Object[] keyElements = ((CompositeKey) key).getKeyElements();
 539                                 return keyElements[index].toString();
 540                         }
 541                 };
 542         }
 543 
 544         public static void addContextMenus(
 545                 IPageContainer pc, ItemHistogram h, String selectionName, IAction ... extraActions) {
 546                 MCContextMenuManager mm = MCContextMenuManager.create(h.getManager().getViewer().getControl());
 547                 ColumnMenusFactory.addDefaultMenus(h.getManager(), mm);
 548                 SelectionStoreActionToolkit.addSelectionStoreActions(pc.getSelectionStore(), h, selectionName, mm);
 549                 for (IAction action : extraActions) {
 550                         mm.add(action);
 551                 }
 552         }
 553 
 554         public static IXDataRenderer buildSizeRow(
 555                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color,
 556                 IColorProvider<IItem> cp) {
 557                 return RendererToolkit.layers(buildSpanRenderer(items, cp),
 558                                 buildTimestampHistogram(title, description, items, a, color));
 559         }
 560 
 561         public static ItemRow buildDurationHistogram(
 562                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color) {
 563                 IQuantitySeries<IQuantity[]> allocationSeries = BucketBuilder.aggregatorSeries(items, a,
 564                                 JfrAttributes.DURATION);
 565                 XYDataRenderer renderer = new XYDataRenderer(getKindOfQuantity(a).getDefaultUnit().quantity(0), title,
 566                                 description);
 567                 renderer.addBarChart(a.getName(), allocationSeries, color);
 568                 return new ItemRow(title, description, renderer, items);
 569         }
 570 
 571         // FIXME: Make something that can use something other than time as x-axis?
 572         public static IXDataRenderer buildSpanRenderer(IItemCollection pathItems, IColorProvider<IItem> cp) {
 573                 ISpanSeries<IItem> dataSeries = QuantitySeries.max(pathItems, JfrAttributes.START_TIME, JfrAttributes.END_TIME);
 574                 return SpanRenderer.withBoundaries(dataSeries, cp);
 575         }
 576 
 577         public static boolean addEndTimeLines(
 578                 XYDataRenderer renderer, IItemCollection items, boolean fill, Stream<IAttribute<IQuantity>> yAttributes) {
 579                 // FIXME: JMC-4520 - Handle multiple item iterables
 580                 Iterator<IItemIterable> ii = items.iterator();
 581                 if (ii.hasNext()) {
 582                         IItemIterable itemStream = ii.next();
 583                         IType<IItem> type = itemStream.getType();
 584                         // FIXME: A better way to ensure sorting by endTime
 585                         return yAttributes.peek(a -> addEndTimeLine(renderer, itemStream.iterator(), type, a, fill))
 586                                         .mapToLong(a -> 1L).sum() > 0;
 587                 }
 588                 return false;
 589         }
 590 
 591         public static void addEndTimeLine(
 592                 XYDataRenderer renderer, Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> yAttribute,
 593                 boolean fill) {
 594                 IQuantitySeries<?> qs = buildQuantitySeries(items, type, JfrAttributes.END_TIME, yAttribute);
 595                 renderer.addLineChart(yAttribute.getName(), qs, getFieldColor(yAttribute), fill);
 596         }
 597 
 598         public static IQuantitySeries<?> buildQuantitySeries(
 599                 Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> xAttribute,
 600                 IAttribute<IQuantity> yAttribute) {
 601                 IMemberAccessor<IQuantity, IItem> yAccessor = yAttribute.getAccessor(type);
 602                 if (yAccessor == null) {
 603                         throw new RuntimeException(yAttribute.getIdentifier() + " is not an attribute for " + type.getIdentifier()); //$NON-NLS-1$
 604                 }
 605                 return buildQuantitySeries(items, type, xAttribute, yAccessor);
 606         }
 607 
 608         public static IQuantitySeries<?> buildQuantitySeries(
 609                 Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> xAttribute,
 610                 IMemberAccessor<? extends IQuantity, IItem> yAccessor) {
 611                 IMemberAccessor<IQuantity, IItem> xAccessor = xAttribute.getAccessor(type);
 612                 return QuantitySeries.all(items, xAccessor, yAccessor);
 613         }
 614 
 615         public static void createChartTooltip(ChartCanvas chart) {
 616                 createChartTooltip(chart, ChartToolTipProvider::new);
 617         }
 618 
 619         public static void createChartTimestampTooltip(ChartCanvas chart) {
 620                 createChartTooltip(chart, JfrAttributes.START_TIME, JfrAttributes.END_TIME, JfrAttributes.DURATION,
 621                                 JfrAttributes.EVENT_TYPE, JfrAttributes.EVENT_STACKTRACE);
 622         }
 623 
 624         public static void createChartTooltip(ChartCanvas chart, IAttribute<?> ... excludedAttributes) {
 625                 createChartTooltip(chart, new HashSet<>(Arrays.asList(excludedAttributes)));
 626         }
 627 
 628         public static void createChartTooltip(ChartCanvas chart, Set<IAttribute<?>> excludedAttributes) {
 629                 createChartTooltip(chart, () -> new ChartToolTipProvider() {
 630                         @Override
 631                         protected Stream<IAttribute<?>> getAttributeStream(IType<IItem> type) {
 632                                 return type.getAttributes().stream().filter(a -> !excludedAttributes.contains(a));
 633                         }
 634                 });
 635         }
 636 
 637         public static void createChartTooltip(ChartCanvas chart, Supplier<ChartToolTipProvider> toolTipProviderSupplier) {
 638                 new ToolTip(chart) {
 639                         String html;
 640                         Map<String, Image> images;
 641 
 642                         @Override
 643                         protected boolean shouldCreateToolTip(Event event) {
 644                                 ChartToolTipProvider provider = toolTipProviderSupplier.get();
 645                                 chart.infoAt(provider, event.x, event.y);
 646                                 html = provider.getHTML();
 647                                 images = provider.getImages();
 648                                 return html != null;
 649                         }
 650 
 651                         @Override
 652                         protected Composite createToolTipContentArea(Event event, Composite parent) {
 653                                 FormText formText = CompositeToolkit.createInfoFormText(parent);
 654                                 for (Map.Entry<String, Image> imgEntry : images.entrySet()) {
 655                                         formText.setImage(imgEntry.getKey(), imgEntry.getValue());
 656                                 }
 657                                 formText.setText(html, true, false);
 658                                 return formText;
 659                         }
 660 
 661                 };
 662 
 663         }
 664 
 665         private static KindOfQuantity<?> getKindOfQuantity(IAggregator<IQuantity, ?> a) {
 666                 IType<? super IQuantity> ct = a.getValueType();
 667                 // FIXME: Refactor to avoid this cast
 668                 return ((KindOfQuantity<?>) ct);
 669         }
 670 
 671         public static Form createForm(Composite parent, FormToolkit toolkit, String title, Image img) {
 672                 Form form = toolkit.createForm(parent);
 673                 form.setText(title.replaceAll("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
 674                 form.setImage(img);
 675                 toolkit.decorateFormHeading(form);
 676                 FillLayout fillLayout = new FillLayout();
 677                 fillLayout.marginHeight = 15;
 678                 fillLayout.marginWidth = 8;
 679                 form.getBody().setLayout(fillLayout);
 680                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_RESULTS));
 681                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_SETUP));
 682                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_NAV));
 683                 return form;
 684         }
 685 
 686         public static class ShowResultAction extends Action {
 687 
 688                 private String[] topics;
 689                 private final IPageContainer pageContainer;
 690                 private volatile Severity maxSeverity;
 691                 private final List<Consumer<Result>> listeners = new ArrayList<>();
 692 
 693                 ShowResultAction(String title, int style, ImageDescriptor icon, Supplier<String> tooltip,
 694                                 IPageContainer pageContainer, String ... topics) {
 695                         super(title, style);
 696                         setImageDescriptor(icon);
 697                         setToolTipText(tooltip.get());
 698                         this.topics = topics;
 699                         this.pageContainer = pageContainer;
 700                         maxSeverity = pageContainer.getRuleManager().getMaxSeverity(topics);
 701                         for (String topic : topics) {
 702                                 Consumer<Result> listener = result -> {
 703                                         Severity severity = Severity.get(result.getScore());
 704                                         if (severity.compareTo(maxSeverity) > 0) {
 705                                                 maxSeverity = severity;
 706                                                 setImageDescriptor(getResultIcon(maxSeverity));
 707                                         } else if (severity.compareTo(maxSeverity) < 0) { // severity could be less than previous max
 708                                                 maxSeverity = pageContainer.getRuleManager().getMaxSeverity(topics);
 709                                         }
 710                                         setToolTipText(tooltip.get());
 711                                 };
 712                                 listeners.add(listener);
 713                                 pageContainer.getRuleManager().addResultListener(topic, listener);
 714                         }
 715                 }
 716 
 717                 private void removeListeners() {
 718                         listeners.forEach(l -> pageContainer.getRuleManager().removeResultListener(l));
 719                 }
 720 
 721                 @Override
 722                 public void run() {
 723                         pageContainer.showResults(topics);
 724                 }
 725         }
 726 
 727         public static void addRuleResultAction(
 728                 Form form, IPageContainer pageContainer, Supplier<String> tooltip, String[] topics) {
 729                 if (topics == null || topics.length == 0 || !FlightRecorderUI.getDefault().isAnalysisEnabled()) {
 730                         return;
 731                 }
 732                 ImageDescriptor icon = getResultIcon(pageContainer.getRuleManager().getMaxSeverity(topics));
 733                 ShowResultAction resultAction = new ShowResultAction(Messages.RULES_SHOW_RESULTS_ACTION, IAction.AS_PUSH_BUTTON,
 734                                 icon, tooltip, pageContainer, topics);
 735                 resultAction.setId(RESULT_ACTION_ID);
 736                 form.getToolBarManager().appendToGroup(DataPageToolkit.FORM_TOOLBAR_PAGE_RESULTS, resultAction);
 737                 form.getToolBarManager().update(true);
 738                 form.addDisposeListener(e -> resultAction.removeListeners());
 739         }
 740 
 741         private static ImageDescriptor getResultIcon(Severity severity) {
 742                 switch (severity) {
 743                 case OK:
 744                         return ResultOverview.ICON_OK;
 745                 case INFO:
 746                         return ResultOverview.ICON_INFO;
 747                 case WARNING:
 748                         return ResultOverview.ICON_WARNING;
 749                 case NA:
 750                         return ResultOverview.ICON_NA;
 751                 }
 752                 return null;
 753         }
 754 
 755         /**
 756          * Return a disabled Action.
 757          *
 758          * @param text
 759          *            text to be displayed by the MenuItem, and represent it as it's id.
 760          * @return an Action containing the desired text, which will be disabled in a UI component.
 761          */
 762         public static IAction disabledAction(String text) {
 763                 IAction disabledAction = new Action(text) {
 764                         @Override
 765                         public boolean isEnabled() {
 766                                 return false;
 767                         }
 768                 };
 769                 disabledAction.setId(text);
 770                 return disabledAction;
 771         }
 772 
 773         public static FilterEditor buildFilterSelector(
 774                 Composite parent, IItemFilter filter, IItemCollection items, Supplier<Stream<SelectionStoreEntry>> selections,
 775                 Consumer<IItemFilter> onSelect, boolean hasBorder) {
 776                 Supplier<Collection<IAttribute<?>>> attributeSupplier = () -> getPersistableAttributes(
 777                                 getAttributes(filter != null ? items.apply(filter) : items)).collect(Collectors.toList());
 778 
 779                 AttributeValueProvider valueSupplier = new AttributeValueProvider() {
 780                         @Override
 781                         public <V> V defaultValue(ICanonicalAccessorFactory<V> attribute) {
 782                                 return findValueForFilter(items, attribute);
 783                         }
 784                 };
 785 
 786                 FilterEditor editor = new FilterEditor(parent, onSelect, filter, attributeSupplier, valueSupplier,
 787                                 TypeLabelProvider::getColorOrDefault, hasBorder ? SWT.BORDER : SWT.NONE);
 788 
 789                 MenuManager addFromSelectionPredicate = new MenuManager(Messages.FILTER_ADD_FROM_SELECTION);
 790                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addFromSelectionPredicate);
 791                 addFromSelectionPredicate.setRemoveAllWhenShown(true);
 792                 addFromSelectionPredicate.addMenuListener(new IMenuListener() {
 793 
 794                         @Override
 795                         public void menuAboutToShow(IMenuManager manager) {
 796                                 selections.get().forEach(entry -> {
 797                                         MenuManager selectionFlavors = new MenuManager(entry.getName());
 798                                         entry.getSelection().getFlavors(editor.getFilter(), items, null)
 799                                                         .filter(f -> f instanceof IFilterFlavor).forEach(flavor -> {
 800                                                                 selectionFlavors.add(new Action(flavor.getName()) {
 801                                                                         @Override
 802                                                                         public void run() {
 803                                                                                 editor.addRoot(((IFilterFlavor) flavor).getFilter());
 804                                                                         }
 805                                                                 });
 806                                                         });
 807                                         if (!selectionFlavors.isEmpty()) {
 808                                                 if (manager.find(Messages.FILTER_NO_SELECTION_AVAILABLE) != null) {
 809                                                         manager.remove(Messages.FILTER_NO_SELECTION_AVAILABLE);
 810                                                 }
 811                                                 manager.add(selectionFlavors);
 812                                         } else {
 813                                                 manager.add(disabledAction(Messages.FILTER_NO_SELECTION_AVAILABLE));
 814                                         }
 815                                 });
 816                         }
 817                 });
 818 
 819                 // FIXME: This could potentially move into the FilterEditor class
 820                 MenuManager addAttributeValuePredicate = new MenuManager(Messages.FILTER_ADD_FROM_ATTRIBUTE);
 821                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addAttributeValuePredicate);
 822                 addAttributeValuePredicate.setRemoveAllWhenShown(true);
 823                 addAttributeValuePredicate.addMenuListener(new IMenuListener() {
 824                         Collection<IAttribute<?>> attributes;
 825 
 826                         @Override
 827                         public void menuAboutToShow(IMenuManager manager) {
 828                                 if (attributes == null) {
 829                                         attributes = attributeSupplier.get();
 830                                 }
 831                                 if (!attributes.isEmpty()) {
 832                                         if (manager.find(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE) != null) {
 833                                                 manager.remove(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE);
 834                                         }
 835                                         attributes.stream().distinct().sorted((a1, a2) -> a1.getName().compareTo(a2.getName()))
 836                                                 .forEach(attr -> {
 837                                                         addAttributeValuePredicate.add(new Action(attr.getName()) {
 838                                                                 @Override
 839                                                                 public void run() {
 840                                                                         IItemFilter filter = createDefaultFilter(items, attr);
 841                                                                         editor.addRoot(filter);
 842                                                                 }
 843                                                         });
 844                                                 });
 845                                 } else {
 846                                         manager.add(disabledAction(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE));
 847                                 }
 848 
 849                         }
 850                 });
 851                 return editor;
 852         }
 853 
 854         // FIXME: Move to some AttributeToolkit?
 855         private static Stream<IAttribute<?>> getAttributes(IItemCollection items) {
 856                 return ItemCollectionToolkit.stream(items).filter(IItemIterable::hasItems)
 857                                 .flatMap(is -> is.getType().getAttributes().stream());
 858         }
 859 
 860         public static Stream<IAttribute<?>> getPersistableAttributes(Stream<IAttribute<?>> attributes) {
 861                 // FIXME: Would like to always be able to persist a string representation of the attribute, because this is usable by filters.
 862 
 863                 // FIXME: Should we always include event type? Does it make any sense, except on the custom pages?
 864 
 865                 // FIXME: Transform both START_TIME and END_TIME to LIFETIME?
 866                 // FIXME: Add derived attributes, like a conversion of any THREAD or CLASS attribute? Thread group?
 867                 /*
 868                  * Make sure to do the conversions in the right order, so for example a stack trace can be
 869                  * converted to a top method, which then is converted to a method string.
 870                  */
 871                 return attributes.map(a -> a.equals(JfrAttributes.EVENT_THREAD) ? JdkAttributes.EVENT_THREAD_NAME : a)
 872                                 .flatMap(a -> a.equals(JfrAttributes.EVENT_STACKTRACE) ? Stream.of(JdkAttributes.STACK_TRACE_STRING,
 873                                                 JdkAttributes.STACK_TRACE_TOP_METHOD_STRING, JdkAttributes.STACK_TRACE_TOP_CLASS_STRING,
 874                                                 JdkAttributes.STACK_TRACE_TOP_PACKAGE, JdkAttributes.STACK_TRACE_BOTTOM_METHOD_STRING)
 875                                                 : Stream.of(a))
 876                                 .map(a -> a.equals(JdkAttributes.COMPILER_METHOD) ? JdkAttributes.COMPILER_METHOD_STRING : a)
 877                                 // FIXME: String or id?
 878                                 .map(a -> a.equals(JdkAttributes.REC_SETTING_FOR) ? JdkAttributes.REC_SETTING_FOR_NAME : a)
 879                                 .map(a -> a.equals(JdkAttributes.CLASS_DEFINING_CLASSLOADER)
 880                                                 ? JdkAttributes.CLASS_DEFINING_CLASSLOADER_STRING : a)
 881                                 .map(a -> a.equals(JdkAttributes.CLASS_INITIATING_CLASSLOADER)
 882                                                 ? JdkAttributes.CLASS_INITIATING_CLASSLOADER_STRING : a)
 883                                 .map(a -> a.equals(JdkAttributes.PARENT_CLASSLOADER)
 884                                                 ? JdkAttributes.PARENT_CLASSLOADER_STRING : a)
 885                                 .map(a -> a.equals(JdkAttributes.CLASSLOADER)
 886                                                 ? JdkAttributes.CLASSLOADER_STRING : a)
 887                                 .filter(a -> a.equals(JfrAttributes.EVENT_TYPE) || (a.getContentType() instanceof RangeContentType)
 888                                                 || (a.getContentType().getPersister() != null))
 889                                 .distinct();
 890         }
 891 
 892         /**
 893          * Returns a value for attribute, firstly by trying to find one in the items, secondly by
 894          * creating a default value for some known content types. Returns null if the first two cases
 895          * fail.
 896          *
 897          * @param items
 898          * @param attribute
 899          * @return a value of type V, or null
 900          */
 901         @SuppressWarnings("unchecked")
 902         private static <V> V findValueForFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 903                 IItem firstItem = ItemCollectionToolkit.stream(items).filter(is -> is.getType().hasAttribute(attribute))
 904                                 .flatMap(ItemIterableToolkit::stream)
 905                                 .filter(i -> ((IMemberAccessor<V, IItem>) attribute.getAccessor(i.getType())).getMember(i) != null)
 906                                 .findFirst().orElse(null);
 907                 if (firstItem != null) {
 908                         IMemberAccessor<V, IItem> accessor = (IMemberAccessor<V, IItem>) attribute.getAccessor(firstItem.getType());
 909                         return accessor.getMember(firstItem);
 910                 }
 911                 if (UnitLookup.PLAIN_TEXT.equals(attribute.getContentType())) {
 912                         return (V) ""; //$NON-NLS-1$
 913                 }
 914                 if (attribute.getContentType() instanceof KindOfQuantity<?>) {
 915                         return (V) ((KindOfQuantity<?>) attribute.getContentType()).getDefaultUnit().quantity(0);
 916                 }
 917                 return null;
 918         }
 919 
 920         /**
 921          * Returns an default filter for attribute, which might be an equals filter, hasAttribute
 922          * filter, or type filter, depending on the attribute and the contents of the items.
 923          *
 924          * @param items
 925          * @param attribute
 926          * @return a filter
 927          */
 928         // FIXME: Should move to FilterEditor, or some subclass/specialization?
 929         private static <V> IItemFilter createDefaultFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 930                 V value = findValueForFilter(items, attribute);
 931                 if (value == null) {
 932                         return ItemFilters.hasAttribute(attribute);
 933                 } else if (attribute.equals(JfrAttributes.EVENT_TYPE)) {
 934                         return ItemFilters.type(((IType<?>) value).getIdentifier());
 935                 }
 936                 return ItemFilters.equals(attribute, value);
 937         }
 938 
 939         public static void addRenameAction(Form form, IPageContainer editor) {
 940                 form.getMenuManager().add(new Action(Messages.PAGE_RENAME_MENU_ACTION) {
 941                         @Override
 942                         public void run() {
 943                                 InputDialog dialog = new InputDialog(form.getShell(), Messages.PAGE_RENAME_DIALOG_TITLE,
 944                                                 Messages.PAGE_RENAME_DIALOG_MESSAGE, form.getText(), null);
 945                                 if (dialog.open() == Window.OK) {
 946                                         form.setText(dialog.getValue());
 947                                         editor.currentPageRefresh();
 948                                 }
 949                         }
 950                 });
 951         }
 952 
 953         public static void addIconChangeAction(Form form, IPageContainer editor, Consumer<Image> newIconConsumer) {
 954                 form.getMenuManager().add(new Action(Messages.PAGE_CHANGE_ICON_MENU_ACTION) {
 955                         @Override
 956                         public void run() {
 957                                 WizardDialog dialog = new WizardDialog(form.getShell(),
 958                                                 new IconChangeWizard(form.getImage(), newIconConsumer));
 959                                 dialog.open();
 960                                 editor.currentPageRefresh();
 961                         }
 962                 });
 963         }
 964 
 965         private static class IconChangeWizard extends Wizard {
 966 
 967                 private final Image currentImage;
 968                 private final Consumer<Image> imageConsumer;
 969                 private Label imageLabel;
 970 
 971                 public IconChangeWizard(Image currentImage, Consumer<Image> imageConsumer) {
 972                         setWindowTitle(Messages.PAGE_CHANGE_ICON_WIZARD_TITLE);
 973                         this.currentImage = currentImage;
 974                         this.imageConsumer = imageConsumer;
 975                 }
 976 
 977                 @Override
 978                 public void addPages() {
 979                         addPage(new WizardPage(Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE) {
 980 
 981                                 @Override
 982                                 public String getTitle() {
 983                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE;
 984                                 }
 985 
 986                                 @Override
 987                                 public String getDescription() {
 988                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_DESC;
 989                                 }
 990 
 991                                 @Override
 992                                 public void createControl(Composite parent) {
 993                                         Composite container = new Composite(parent, SWT.NONE);
 994                                         GridLayout layout = new GridLayout(1, false);
 995                                         container.setLayout(layout);
 996 
 997                                         Button button = new Button(container, SWT.NONE);
 998                                         button.setText(Messages.PAGE_CHANGE_ICON_CHOOSE_IMAGE_FILE);
 999 
1000                                         button.addSelectionListener(new SelectionAdapter() {
1001                                                 @Override
1002                                                 public void widgetSelected(SelectionEvent e) {
1003                                                         chooseImageFileDialog();
1004                                                 }
1005                                         });
1006 
1007                                         if (currentImage != null) {
1008                                                 new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_CURRENT_ICON);
1009                                                 new Label(container, SWT.BORDER).setImage(currentImage);
1010                                         }
1011                                         new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_NEW_ICON_PREVIEW);
1012                                         imageLabel = new Label(container, SWT.BORDER);
1013                                         GridData gd = new GridData(16, 16);
1014                                         imageLabel.setLayoutData(gd);
1015 
1016                                         setControl(container);
1017                                 }
1018 
1019                                 private void chooseImageFileDialog() {
1020                                         FileDialog fileDialog = new FileDialog(getShell(), SWT.OPEN);
1021                                         String[] filterNames = new String[] {"Image Files", "All Files (*)"}; //$NON-NLS-1$ //$NON-NLS-2$
1022                                         String[] filterExtensions = new String[] {"*.gif;*.png;*.xpm;*.jpg;*.jpeg;*.tiff", "*"}; //$NON-NLS-1$ //$NON-NLS-2$
1023                                         fileDialog.setFilterNames(filterNames);
1024                                         fileDialog.setFilterExtensions(filterExtensions);
1025                                         String filename = fileDialog.open();
1026                                         if (filename == null) {
1027                                                 // Dialog was cancelled. Bail out early to avoid handling that case later. Premature?
1028                                                 return;
1029                                         }
1030                                         try (InputStream fis = new FileInputStream(filename)) {
1031                                                 ImageData imageData = new ImageData(fis);
1032                                                 // Validate image data
1033                                                 if (imageData.width != 16 || imageData.height != 16) {
1034                                                         imageData = resizeImage(imageData, 16, 16);
1035                                                 }
1036                                                 DisplayToolkit.dispose(imageLabel.getImage());
1037                                                 imageLabel.setImage(new Image(getShell().getDisplay(), imageData));
1038                                                 imageLabel.getParent().layout();
1039                                                 setPageComplete(isPageComplete());
1040                                         } catch (Exception e) {
1041                                                 // FIXME: Add proper logging
1042                                                 e.printStackTrace();
1043                                         }
1044                                 }
1045 
1046                                 private ImageData resizeImage(ImageData imageData, int width, int height) {
1047                                         Image original = ImageDescriptor.createFromImageData(imageData).createImage();
1048                                         Image scaled = new Image(Display.getDefault(), width, height);
1049                                         GC gc = new GC(scaled);
1050                                         gc.setAntialias(SWT.ON);
1051                                         gc.setInterpolation(SWT.HIGH);
1052                                         gc.drawImage(original, 0, 0, imageData.width, imageData.height, 0, 0, width, height);
1053                                         gc.dispose();
1054                                         original.dispose();
1055                                         ImageData scaledData = scaled.getImageData();
1056                                         scaled.dispose();
1057                                         return scaledData;
1058                                 }
1059 
1060                                 @Override
1061                                 public boolean isPageComplete() {
1062                                         return imageLabel.getImage() != null;
1063                                 }
1064 
1065                         });
1066                 }
1067 
1068                 @Override
1069                 public boolean performFinish() {
1070                         imageConsumer.accept(imageLabel.getImage());
1071                         DisplayToolkit.dispose(currentImage);
1072                         return true;
1073                 }
1074 
1075                 @Override
1076                 public boolean performCancel() {
1077                         DisplayToolkit.dispose(imageLabel.getImage());
1078                         return true;
1079                 }
1080 
1081         }
1082 
1083         public static ItemList createSimpleItemList(
1084                 Composite parent, ItemListBuilder listBuilder, IPageContainer pageContainer, TableSettings tableSettings,
1085                 String selectionName) {
1086 
1087                 ItemList list = listBuilder.build(parent, tableSettings);
1088                 ColumnViewer viewer = list.getManager().getViewer();
1089                 MCContextMenuManager mm = MCContextMenuManager.create(viewer.getControl());
1090                 ColumnMenusFactory.addDefaultMenus(list.getManager(), mm);
1091                 viewer.addSelectionChangedListener(
1092                                 e -> pageContainer.showSelection(ItemCollectionToolkit.build(list.getSelection().get())));
1093 
1094                 if (selectionName != null) {
1095                         SelectionStoreActionToolkit.addSelectionStoreActions(pageContainer.getSelectionStore(), list, selectionName,
1096                                         mm);
1097                 }
1098 
1099                 return list;
1100         }
1101 
1102         public static void addTabItem(CTabFolder tabFolder, Control section, String name) {
1103                 CTabItem tabItem = new CTabItem(tabFolder, SWT.NONE);
1104                 tabItem.setControl(section);
1105                 tabItem.setText(name);
1106         }
1107 
1108         public static TypeFilterBuilder buildEventTypeTree(
1109                 Composite parent, FormToolkit toolkit, Runnable onChange, boolean checkbox) {
1110                 // TODO: Make more accessible.
1111                 // TODO: Add support for storing the expansion state in a memento.
1112                 // TODO: Add input from selection store, output to selection store
1113                 // TODO: Add toolbar for choosing tree or checkbox tree.
1114                 Composite treeComposite = new Composite(parent, SWT.NONE);
1115                 treeComposite.setLayout(new GridLayout());
1116                 toolkit.adapt(treeComposite);
1117                 Label caption = toolkit.createLabel(treeComposite, Messages.EVENT_TYPE_TREE_TITLE);
1118                 caption.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
1119                 caption.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
1120 
1121                 TypeFilterBuilder typeFilterTree = new TypeFilterBuilder(treeComposite, onChange, checkbox);
1122 
1123                 typeFilterTree.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
1124                 return typeFilterTree;
1125         }
1126 
1127         public static boolean isTypeWithThreadAndDuration(IType<?> type) {
1128                 return JfrAttributes.EVENT_THREAD.getAccessor(type) != null
1129                                 && JfrAttributes.START_TIME.getAccessor(type) != JfrAttributes.END_TIME.getAccessor(type);
1130         }
1131 
1132         public static void addPage(Set<IType<?>> selectedTypes) {
1133                 PageManager pm = FlightRecorderUI.getDefault().getPageManager();
1134                 pm.makeRoot(pm.createPage(ItemHandlerPage.Factory.class, new ItemHandlerUiStandIn(selectedTypes)));
1135         }
1136 }