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