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