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(":") //$NON-NLS-1$
 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         /**
 753          * Return a disabled Action.
 754          *
 755          * @param text
 756          *            text to be displayed by the MenuItem, and represent it as it's id.
 757          * @return an Action containing the desired text, which will be disabled in a UI component.
 758          */
 759         public static IAction disabledAction(String text) {
 760                 IAction disabledAction = new Action(text) {
 761                         @Override
 762                         public boolean isEnabled() {
 763                                 return false;
 764                         }
 765                 };
 766                 disabledAction.setId(text);
 767                 return disabledAction;
 768         }
 769 
 770         public static FilterEditor buildFilterSelector(
 771                 Composite parent, IItemFilter filter, IItemCollection items, Supplier<Stream<SelectionStoreEntry>> selections,
 772                 Consumer<IItemFilter> onSelect, boolean hasBorder) {
 773                 Supplier<Collection<IAttribute<?>>> attributeSupplier = () -> getPersistableAttributes(
 774                                 getAttributes(filter != null ? items.apply(filter) : items)).collect(Collectors.toList());
 775 
 776                 AttributeValueProvider valueSupplier = new AttributeValueProvider() {
 777                         @Override
 778                         public <V> V defaultValue(ICanonicalAccessorFactory<V> attribute) {
 779                                 return findValueForFilter(items, attribute);
 780                         }
 781                 };
 782 
 783                 FilterEditor editor = new FilterEditor(parent, onSelect, filter, attributeSupplier, valueSupplier,
 784                                 TypeLabelProvider::getColorOrDefault, hasBorder ? SWT.BORDER : SWT.NONE);
 785 
 786                 MenuManager addFromSelectionPredicate = new MenuManager(Messages.FILTER_ADD_FROM_SELECTION);
 787                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addFromSelectionPredicate);
 788                 addFromSelectionPredicate.setRemoveAllWhenShown(true);
 789                 addFromSelectionPredicate.addMenuListener(new IMenuListener() {
 790 
 791                         @Override
 792                         public void menuAboutToShow(IMenuManager manager) {
 793                                 selections.get().forEach(entry -> {
 794                                         MenuManager selectionFlavors = new MenuManager(entry.getName());
 795                                         entry.getSelection().getFlavors(editor.getFilter(), items, null)
 796                                                         .filter(f -> f instanceof IFilterFlavor).forEach(flavor -> {
 797                                                                 selectionFlavors.add(new Action(flavor.getName()) {
 798                                                                         @Override
 799                                                                         public void run() {
 800                                                                                 editor.addRoot(((IFilterFlavor) flavor).getFilter());
 801                                                                         }
 802                                                                 });
 803                                                         });
 804                                         if (!selectionFlavors.isEmpty()) {
 805                                                 if (manager.find(Messages.FILTER_NO_SELECTION_AVAILABLE) != null) {
 806                                                         manager.remove(Messages.FILTER_NO_SELECTION_AVAILABLE);
 807                                                 }
 808                                                 manager.add(selectionFlavors);
 809                                         } else {
 810                                                 manager.add(disabledAction(Messages.FILTER_NO_SELECTION_AVAILABLE));
 811                                         }
 812                                 });
 813                         }
 814                 });
 815 
 816                 // FIXME: This could potentially move into the FilterEditor class
 817                 MenuManager addAttributeValuePredicate = new MenuManager(Messages.FILTER_ADD_FROM_ATTRIBUTE);
 818                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addAttributeValuePredicate);
 819                 addAttributeValuePredicate.setRemoveAllWhenShown(true);
 820                 addAttributeValuePredicate.addMenuListener(new IMenuListener() {
 821                         Collection<IAttribute<?>> attributes;
 822 
 823                         @Override
 824                         public void menuAboutToShow(IMenuManager manager) {
 825                                 if (attributes == null) {
 826                                         attributes = attributeSupplier.get();
 827                                 }
 828                                 if (!attributes.isEmpty()) {
 829                                         if (manager.find(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE) != null) {
 830                                                 manager.remove(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE);
 831                                         }
 832                                         attributes.stream().distinct().sorted((a1, a2) -> a1.getName().compareTo(a2.getName()))
 833                                                 .forEach(attr -> {
 834                                                         addAttributeValuePredicate.add(new Action(attr.getName()) {
 835                                                                 @Override
 836                                                                 public void run() {
 837                                                                         IItemFilter filter = createDefaultFilter(items, attr);
 838                                                                         editor.addRoot(filter);
 839                                                                 }
 840                                                         });
 841                                                 });
 842                                 } else {
 843                                         manager.add(disabledAction(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE));
 844                                 }
 845 
 846                         }
 847                 });
 848                 return editor;
 849         }
 850 
 851         // FIXME: Move to some AttributeToolkit?
 852         private static Stream<IAttribute<?>> getAttributes(IItemCollection items) {
 853                 return ItemCollectionToolkit.stream(items).filter(IItemIterable::hasItems)
 854                                 .flatMap(is -> is.getType().getAttributes().stream());
 855         }
 856 
 857         public static Stream<IAttribute<?>> getPersistableAttributes(Stream<IAttribute<?>> attributes) {
 858                 // FIXME: Would like to always be able to persist a string representation of the attribute, because this is usable by filters.
 859 
 860                 // FIXME: Should we always include event type? Does it make any sense, except on the custom pages?
 861 
 862                 // FIXME: Transform both START_TIME and END_TIME to LIFETIME?
 863                 // FIXME: Add derived attributes, like a conversion of any THREAD or CLASS attribute? Thread group?
 864                 /*
 865                  * Make sure to do the conversions in the right order, so for example a stack trace can be
 866                  * converted to a top method, which then is converted to a method string.
 867                  */
 868                 return attributes.map(a -> a.equals(JfrAttributes.EVENT_THREAD) ? JdkAttributes.EVENT_THREAD_NAME : a)
 869                                 .flatMap(a -> a.equals(JfrAttributes.EVENT_STACKTRACE) ? Stream.of(JdkAttributes.STACK_TRACE_STRING,
 870                                                 JdkAttributes.STACK_TRACE_TOP_METHOD_STRING, JdkAttributes.STACK_TRACE_TOP_CLASS_STRING,
 871                                                 JdkAttributes.STACK_TRACE_TOP_PACKAGE, JdkAttributes.STACK_TRACE_BOTTOM_METHOD_STRING)
 872                                                 : Stream.of(a))
 873                                 .map(a -> a.equals(JdkAttributes.COMPILER_METHOD) ? JdkAttributes.COMPILER_METHOD_STRING : a)
 874                                 // FIXME: String or id?
 875                                 .map(a -> a.equals(JdkAttributes.REC_SETTING_FOR) ? JdkAttributes.REC_SETTING_FOR_NAME : a)
 876                                 .map(a -> a.equals(JdkAttributes.CLASS_DEFINING_CLASSLOADER)
 877                                                 ? JdkAttributes.CLASS_DEFINING_CLASSLOADER_STRING : a)
 878                                 .map(a -> a.equals(JdkAttributes.CLASS_INITIATING_CLASSLOADER)
 879                                                 ? JdkAttributes.CLASS_INITIATING_CLASSLOADER_STRING : a)
 880                                 .map(a -> a.equals(JdkAttributes.PARENT_CLASSLOADER)
 881                                                 ? JdkAttributes.PARENT_CLASSLOADER_STRING : a)
 882                                 .map(a -> a.equals(JdkAttributes.CLASSLOADER)
 883                                                 ? JdkAttributes.CLASSLOADER_STRING : a)
 884                                 .filter(a -> a.equals(JfrAttributes.EVENT_TYPE) || (a.getContentType() instanceof RangeContentType)
 885                                                 || (a.getContentType().getPersister() != null))
 886                                 .distinct();
 887         }
 888 
 889         /**
 890          * Returns a value for attribute, firstly by trying to find one in the items, secondly by
 891          * creating a default value for some known content types. Returns null if the first two cases
 892          * fail.
 893          *
 894          * @param items
 895          * @param attribute
 896          * @return a value of type V, or null
 897          */
 898         @SuppressWarnings("unchecked")
 899         private static <V> V findValueForFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 900                 IItem firstItem = ItemCollectionToolkit.stream(items).filter(is -> is.getType().hasAttribute(attribute))
 901                                 .flatMap(ItemIterableToolkit::stream)
 902                                 .filter(i -> ((IMemberAccessor<V, IItem>) attribute.getAccessor(i.getType())).getMember(i) != null)
 903                                 .findFirst().orElse(null);
 904                 if (firstItem != null) {
 905                         IMemberAccessor<V, IItem> accessor = (IMemberAccessor<V, IItem>) attribute.getAccessor(firstItem.getType());
 906                         return accessor.getMember(firstItem);
 907                 }
 908                 if (UnitLookup.PLAIN_TEXT.equals(attribute.getContentType())) {
 909                         return (V) ""; //$NON-NLS-1$
 910                 }
 911                 if (attribute.getContentType() instanceof KindOfQuantity<?>) {
 912                         return (V) ((KindOfQuantity<?>) attribute.getContentType()).getDefaultUnit().quantity(0);
 913                 }
 914                 return null;
 915         }
 916 
 917         /**
 918          * Returns an default filter for attribute, which might be an equals filter, hasAttribute
 919          * filter, or type filter, depending on the attribute and the contents of the items.
 920          *
 921          * @param items
 922          * @param attribute
 923          * @return a filter
 924          */
 925         // FIXME: Should move to FilterEditor, or some subclass/specialization?
 926         private static <V> IItemFilter createDefaultFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 927                 V value = findValueForFilter(items, attribute);
 928                 if (value == null) {
 929                         return ItemFilters.hasAttribute(attribute);
 930                 } else if (attribute.equals(JfrAttributes.EVENT_TYPE)) {
 931                         return ItemFilters.type(((IType<?>) value).getIdentifier());
 932                 }
 933                 return ItemFilters.equals(attribute, value);
 934         }
 935 
 936         public static void addRenameAction(Form form, IPageContainer editor) {
 937                 form.getMenuManager().add(new Action(Messages.PAGE_RENAME_MENU_ACTION) {
 938                         @Override
 939                         public void run() {
 940                                 InputDialog dialog = new InputDialog(form.getShell(), Messages.PAGE_RENAME_DIALOG_TITLE,
 941                                                 Messages.PAGE_RENAME_DIALOG_MESSAGE, form.getText(), null);
 942                                 if (dialog.open() == Window.OK) {
 943                                         form.setText(dialog.getValue());
 944                                         editor.currentPageRefresh();
 945                                 }
 946                         }
 947                 });
 948         }
 949 
 950         public static void addIconChangeAction(Form form, IPageContainer editor, Consumer<Image> newIconConsumer) {
 951                 form.getMenuManager().add(new Action(Messages.PAGE_CHANGE_ICON_MENU_ACTION) {
 952                         @Override
 953                         public void run() {
 954                                 WizardDialog dialog = new WizardDialog(form.getShell(),
 955                                                 new IconChangeWizard(form.getImage(), newIconConsumer));
 956                                 dialog.open();
 957                                 editor.currentPageRefresh();
 958                         }
 959                 });
 960         }
 961 
 962         private static class IconChangeWizard extends Wizard {
 963 
 964                 private final Image currentImage;
 965                 private final Consumer<Image> imageConsumer;
 966                 private Label imageLabel;
 967 
 968                 public IconChangeWizard(Image currentImage, Consumer<Image> imageConsumer) {
 969                         setWindowTitle(Messages.PAGE_CHANGE_ICON_WIZARD_TITLE);
 970                         this.currentImage = currentImage;
 971                         this.imageConsumer = imageConsumer;
 972                 }
 973 
 974                 @Override
 975                 public void addPages() {
 976                         addPage(new WizardPage(Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE) {
 977 
 978                                 @Override
 979                                 public String getTitle() {
 980                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE;
 981                                 }
 982 
 983                                 @Override
 984                                 public String getDescription() {
 985                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_DESC;
 986                                 }
 987 
 988                                 @Override
 989                                 public void createControl(Composite parent) {
 990                                         Composite container = new Composite(parent, SWT.NONE);
 991                                         GridLayout layout = new GridLayout(1, false);
 992                                         container.setLayout(layout);
 993 
 994                                         Button button = new Button(container, SWT.NONE);
 995                                         button.setText(Messages.PAGE_CHANGE_ICON_CHOOSE_IMAGE_FILE);
 996 
 997                                         button.addSelectionListener(new SelectionAdapter() {
 998                                                 @Override
 999                                                 public void widgetSelected(SelectionEvent e) {
1000                                                         chooseImageFileDialog();
1001                                                 }
1002                                         });
1003 
1004                                         if (currentImage != null) {
1005                                                 new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_CURRENT_ICON);
1006                                                 new Label(container, SWT.BORDER).setImage(currentImage);
1007                                         }
1008                                         new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_NEW_ICON_PREVIEW);
1009                                         imageLabel = new Label(container, SWT.BORDER);
1010                                         GridData gd = new GridData(16, 16);
1011                                         imageLabel.setLayoutData(gd);
1012 
1013                                         setControl(container);
1014                                 }
1015 
1016                                 private void chooseImageFileDialog() {
1017                                         FileDialog fileDialog = new FileDialog(getShell(), SWT.OPEN);
1018                                         String[] filterNames = new String[] {"Image Files", "All Files (*)"}; //$NON-NLS-1$ //$NON-NLS-2$
1019                                         String[] filterExtensions = new String[] {"*.gif;*.png;*.xpm;*.jpg;*.jpeg;*.tiff", "*"}; //$NON-NLS-1$ //$NON-NLS-2$
1020                                         fileDialog.setFilterNames(filterNames);
1021                                         fileDialog.setFilterExtensions(filterExtensions);
1022                                         String filename = fileDialog.open();
1023                                         if (filename == null) {
1024                                                 // Dialog was cancelled. Bail out early to avoid handling that case later. Premature?
1025                                                 return;
1026                                         }
1027                                         try (InputStream fis = new FileInputStream(filename)) {
1028                                                 ImageData imageData = new ImageData(fis);
1029                                                 // Validate image data
1030                                                 if (imageData.width != 16 || imageData.height != 16) {
1031                                                         imageData = resizeImage(imageData, 16, 16);
1032                                                 }
1033                                                 DisplayToolkit.dispose(imageLabel.getImage());
1034                                                 imageLabel.setImage(new Image(getShell().getDisplay(), imageData));
1035                                                 imageLabel.getParent().layout();
1036                                                 setPageComplete(isPageComplete());
1037                                         } catch (Exception e) {
1038                                                 // FIXME: Add proper logging
1039                                                 e.printStackTrace();
1040                                         }
1041                                 }
1042 
1043                                 private ImageData resizeImage(ImageData imageData, int width, int height) {
1044                                         Image original = ImageDescriptor.createFromImageData(imageData).createImage();
1045                                         Image scaled = new Image(Display.getDefault(), width, height);
1046                                         GC gc = new GC(scaled);
1047                                         gc.setAntialias(SWT.ON);
1048                                         gc.setInterpolation(SWT.HIGH);
1049                                         gc.drawImage(original, 0, 0, imageData.width, imageData.height, 0, 0, width, height);
1050                                         gc.dispose();
1051                                         original.dispose();
1052                                         ImageData scaledData = scaled.getImageData();
1053                                         scaled.dispose();
1054                                         return scaledData;
1055                                 }
1056 
1057                                 @Override
1058                                 public boolean isPageComplete() {
1059                                         return imageLabel.getImage() != null;
1060                                 }
1061 
1062                         });
1063                 }
1064 
1065                 @Override
1066                 public boolean performFinish() {
1067                         imageConsumer.accept(imageLabel.getImage());
1068                         DisplayToolkit.dispose(currentImage);
1069                         return true;
1070                 }
1071 
1072                 @Override
1073                 public boolean performCancel() {
1074                         DisplayToolkit.dispose(imageLabel.getImage());
1075                         return true;
1076                 }
1077 
1078         }
1079 
1080         public static ItemList createSimpleItemList(
1081                 Composite parent, ItemListBuilder listBuilder, IPageContainer pageContainer, TableSettings tableSettings,
1082                 String selectionName) {
1083 
1084                 ItemList list = listBuilder.build(parent, tableSettings);
1085                 ColumnViewer viewer = list.getManager().getViewer();
1086                 MCContextMenuManager mm = MCContextMenuManager.create(viewer.getControl());
1087                 ColumnMenusFactory.addDefaultMenus(list.getManager(), mm);
1088                 viewer.addSelectionChangedListener(
1089                                 e -> pageContainer.showSelection(ItemCollectionToolkit.build(list.getSelection().get())));
1090 
1091                 if (selectionName != null) {
1092                         SelectionStoreActionToolkit.addSelectionStoreActions(pageContainer.getSelectionStore(), list, selectionName,
1093                                         mm);
1094                 }
1095 
1096                 return list;
1097         }
1098 
1099         public static void addTabItem(CTabFolder tabFolder, Control section, String name) {
1100                 CTabItem tabItem = new CTabItem(tabFolder, SWT.NONE);
1101                 tabItem.setControl(section);
1102                 tabItem.setText(name);
1103         }
1104 
1105         public static TypeFilterBuilder buildEventTypeTree(
1106                 Composite parent, FormToolkit toolkit, Runnable onChange, boolean checkbox) {
1107                 // TODO: Make more accessible.
1108                 // TODO: Add support for storing the expansion state in a memento.
1109                 // TODO: Add input from selection store, output to selection store
1110                 // TODO: Add toolbar for choosing tree or checkbox tree.
1111                 Composite treeComposite = new Composite(parent, SWT.NONE);
1112                 treeComposite.setLayout(new GridLayout());
1113                 toolkit.adapt(treeComposite);
1114                 Label caption = toolkit.createLabel(treeComposite, Messages.EVENT_TYPE_TREE_TITLE);
1115                 caption.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
1116                 caption.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
1117 
1118                 TypeFilterBuilder typeFilterTree = new TypeFilterBuilder(treeComposite, onChange, checkbox);
1119 
1120                 typeFilterTree.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
1121                 return typeFilterTree;
1122         }
1123 
1124         public static boolean isTypeWithThreadAndDuration(IType<?> type) {
1125                 return JfrAttributes.EVENT_THREAD.getAccessor(type) != null
1126                                 && JfrAttributes.START_TIME.getAccessor(type) != JfrAttributes.END_TIME.getAccessor(type);
1127         }
1128 
1129 }