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                 ItemHistogram table = histogramBuilder.buildWithoutBorder(parent, settings);
 518                 return table;
 519         }
 520 
 521         public static IBaseLabelProvider createTableHighlightProvider(Pattern highlightPattern, boolean isWarning) {
 522                 return new StyledCellLabelProvider() {
 523                         @Override
 524                         public void update(ViewerCell cell) {
 525                                 org.eclipse.swt.graphics.Color color = isWarning
 526                                                 ? new org.eclipse.swt.graphics.Color(Display.getCurrent(), 240, 120, 140)
 527                                                 : new org.eclipse.swt.graphics.Color(Display.getCurrent(), 255, 144, 4);
 528                                 String text = getText(cell.getElement(), cell.getColumnIndex());
 529                                 Matcher matcher = highlightPattern.matcher(text);
 530                                 if (matcher.find()) {
 531                                         cell.getViewerRow().setBackground(0, color);
 532                                         cell.getViewerRow().setBackground(1, color);
 533                                 }
 534                                 cell.setText(text);
 535                                 super.update(cell);
 536                         }
 537 
 538                         private String getText(Object element, int index) {
 539                                 Object key = AggregationGrid.getKey(element);
 540                                 Object[] keyElements = ((CompositeKey) key).getKeyElements();
 541                                 return keyElements[index].toString();
 542                         }
 543                 };
 544         }
 545 
 546         public static void addContextMenus(
 547                 IPageContainer pc, ItemHistogram h, String selectionName, IAction ... extraActions) {
 548                 MCContextMenuManager mm = MCContextMenuManager.create(h.getManager().getViewer().getControl());
 549                 ColumnMenusFactory.addDefaultMenus(h.getManager(), mm);
 550                 SelectionStoreActionToolkit.addSelectionStoreActions(pc.getSelectionStore(), h, selectionName, mm);
 551                 for (IAction action : extraActions) {
 552                         mm.add(action);
 553                 }
 554         }
 555 
 556         public static IXDataRenderer buildSizeRow(
 557                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color,
 558                 IColorProvider<IItem> cp) {
 559                 return RendererToolkit.layers(buildSpanRenderer(items, cp),
 560                                 buildTimestampHistogram(title, description, items, a, color));
 561         }
 562 
 563         public static ItemRow buildDurationHistogram(
 564                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color) {
 565                 IQuantitySeries<IQuantity[]> allocationSeries = BucketBuilder.aggregatorSeries(items, a,
 566                                 JfrAttributes.DURATION);
 567                 XYDataRenderer renderer = new XYDataRenderer(getKindOfQuantity(a).getDefaultUnit().quantity(0), title,
 568                                 description);
 569                 renderer.addBarChart(a.getName(), allocationSeries, color);
 570                 return new ItemRow(title, description, renderer, items);
 571         }
 572 
 573         public static ItemRow buildSizeHistogram(
 574                 String title, String description, IItemCollection items, IAggregator<IQuantity, ?> a, Color color, IAttribute<IQuantity> attribute) {
 575                 IQuantitySeries<IQuantity[]> allocationSeries = BucketBuilder.aggregatorSeries(items, a,
 576                                 JdkAttributes.IO_SIZE);
 577                 XYDataRenderer renderer = new XYDataRenderer(getKindOfQuantity(a).getDefaultUnit().quantity(0), title,
 578                                 description);
 579                 renderer.addBarChart(a.getName(), allocationSeries, color);
 580                 return new ItemRow(title, description, renderer, items);
 581         }
 582 
 583         public static IRange<IQuantity> buildSizeRange(IItemCollection items, boolean isSocket){
 584                 IQuantity end = null;
 585                 if(isSocket) {
 586                         end = QuantitiesToolkit.maxPresent(items.getAggregate(JdkAggregators.SOCKET_READ_LARGEST),
 587                                         items.getAggregate(JdkAggregators.SOCKET_WRITE_LARGEST));
 588                 } else {
 589                         end = QuantitiesToolkit.maxPresent(items.getAggregate(JdkAggregators.FILE_READ_LARGEST),
 590                                         items.getAggregate(JdkAggregators.FILE_WRITE_LARGEST));
 591                 }
 592                 end = end == null ? UnitLookup.BYTE.quantity(1024) : end;
 593                 return QuantityRange.createWithEnd(UnitLookup.BYTE.quantity(0), end);
 594         }
 595 
 596         // FIXME: Make something that can use something other than time as x-axis?
 597         public static IXDataRenderer buildSpanRenderer(IItemCollection pathItems, IColorProvider<IItem> cp) {
 598                 ISpanSeries<IItem> dataSeries = QuantitySeries.max(pathItems, JfrAttributes.START_TIME, JfrAttributes.END_TIME);
 599                 return SpanRenderer.withBoundaries(dataSeries, cp);
 600         }
 601 
 602         public static boolean addEndTimeLines(
 603                 XYDataRenderer renderer, IItemCollection items, boolean fill, Stream<IAttribute<IQuantity>> yAttributes) {
 604                 // FIXME: JMC-4520 - Handle multiple item iterables
 605                 Iterator<IItemIterable> ii = items.iterator();
 606                 if (ii.hasNext()) {
 607                         IItemIterable itemStream = ii.next();
 608                         IType<IItem> type = itemStream.getType();
 609                         // FIXME: A better way to ensure sorting by endTime
 610                         return yAttributes.peek(a -> addEndTimeLine(renderer, itemStream.iterator(), type, a, fill))
 611                                         .mapToLong(a -> 1L).sum() > 0;
 612                 }
 613                 return false;
 614         }
 615 
 616         public static void addEndTimeLine(
 617                 XYDataRenderer renderer, Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> yAttribute,
 618                 boolean fill) {
 619                 IQuantitySeries<?> qs = buildQuantitySeries(items, type, JfrAttributes.END_TIME, yAttribute);
 620                 renderer.addLineChart(yAttribute.getName(), qs, getFieldColor(yAttribute), fill);
 621         }
 622 
 623         public static IQuantitySeries<?> buildQuantitySeries(
 624                 Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> xAttribute,
 625                 IAttribute<IQuantity> yAttribute) {
 626                 IMemberAccessor<IQuantity, IItem> yAccessor = yAttribute.getAccessor(type);
 627                 if (yAccessor == null) {
 628                         throw new RuntimeException(yAttribute.getIdentifier() + " is not an attribute for " + type.getIdentifier()); //$NON-NLS-1$
 629                 }
 630                 return buildQuantitySeries(items, type, xAttribute, yAccessor);
 631         }
 632 
 633         public static IQuantitySeries<?> buildQuantitySeries(
 634                 Iterator<? extends IItem> items, IType<IItem> type, IAttribute<IQuantity> xAttribute,
 635                 IMemberAccessor<? extends IQuantity, IItem> yAccessor) {
 636                 IMemberAccessor<IQuantity, IItem> xAccessor = xAttribute.getAccessor(type);
 637                 return QuantitySeries.all(items, xAccessor, yAccessor);
 638         }
 639 
 640         public static void createChartTooltip(ChartCanvas chart) {
 641                 createChartTooltip(chart, ChartToolTipProvider::new);
 642         }
 643 
 644         public static void createChartTimestampTooltip(ChartCanvas chart) {
 645                 createChartTooltip(chart, JfrAttributes.START_TIME, JfrAttributes.END_TIME, JfrAttributes.DURATION,
 646                                 JfrAttributes.EVENT_TYPE, JfrAttributes.EVENT_STACKTRACE);
 647         }
 648 
 649         public static void createChartTooltip(ChartCanvas chart, IAttribute<?> ... excludedAttributes) {
 650                 createChartTooltip(chart, new HashSet<>(Arrays.asList(excludedAttributes)));
 651         }
 652 
 653         public static void createChartTooltip(ChartCanvas chart, Set<IAttribute<?>> excludedAttributes) {
 654                 createChartTooltip(chart, () -> new ChartToolTipProvider() {
 655                         @Override
 656                         protected Stream<IAttribute<?>> getAttributeStream(IType<IItem> type) {
 657                                 return type.getAttributes().stream().filter(a -> !excludedAttributes.contains(a));
 658                         }
 659                 });
 660         }
 661 
 662         public static void createChartTooltip(ChartCanvas chart, Supplier<ChartToolTipProvider> toolTipProviderSupplier) {
 663                 new ToolTip(chart) {
 664                         String html;
 665                         Map<String, Image> images;
 666 
 667                         @Override
 668                         protected boolean shouldCreateToolTip(Event event) {
 669                                 ChartToolTipProvider provider = toolTipProviderSupplier.get();
 670                                 chart.infoAt(provider, event.x, event.y);
 671                                 html = provider.getHTML();
 672                                 images = provider.getImages();
 673                                 return html != null;
 674                         }
 675 
 676                         @Override
 677                         protected Composite createToolTipContentArea(Event event, Composite parent) {
 678                                 FormText formText = CompositeToolkit.createInfoFormText(parent);
 679                                 for (Map.Entry<String, Image> imgEntry : images.entrySet()) {
 680                                         formText.setImage(imgEntry.getKey(), imgEntry.getValue());
 681                                 }
 682                                 formText.setText(html, true, false);
 683                                 return formText;
 684                         }
 685 
 686                 };
 687 
 688         }
 689 
 690         private static KindOfQuantity<?> getKindOfQuantity(IAggregator<IQuantity, ?> a) {
 691                 IType<? super IQuantity> ct = a.getValueType();
 692                 // FIXME: Refactor to avoid this cast
 693                 return ((KindOfQuantity<?>) ct);
 694         }
 695 
 696         public static Form createForm(Composite parent, FormToolkit toolkit, String title, Image img) {
 697                 Form form = toolkit.createForm(parent);
 698                 form.setText(title.replaceAll("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
 699                 form.setImage(img);
 700                 toolkit.decorateFormHeading(form);
 701                 FillLayout fillLayout = new FillLayout();
 702                 fillLayout.marginHeight = 15;
 703                 fillLayout.marginWidth = 8;
 704                 form.getBody().setLayout(fillLayout);
 705                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_RESULTS));
 706                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_SETUP));
 707                 form.getToolBarManager().add(new Separator(FORM_TOOLBAR_PAGE_NAV));
 708                 return form;
 709         }
 710 
 711         public static class ShowResultAction extends Action {
 712 
 713                 private String[] topics;
 714                 private final IPageContainer pageContainer;
 715                 private volatile Severity maxSeverity;
 716                 private final List<Consumer<Result>> listeners = new ArrayList<>();
 717 
 718                 ShowResultAction(String title, int style, ImageDescriptor icon, Supplier<String> tooltip,
 719                                 IPageContainer pageContainer, String ... topics) {
 720                         super(title, style);
 721                         setImageDescriptor(icon);
 722                         setToolTipText(tooltip.get());
 723                         this.topics = topics;
 724                         this.pageContainer = pageContainer;
 725                         maxSeverity = pageContainer.getRuleManager().getMaxSeverity(topics);
 726                         for (String topic : topics) {
 727                                 Consumer<Result> listener = result -> {
 728                                         Severity severity = Severity.get(result.getScore());
 729                                         if (severity.compareTo(maxSeverity) > 0) {
 730                                                 maxSeverity = severity;
 731                                                 setImageDescriptor(getResultIcon(maxSeverity));
 732                                         } else if (severity.compareTo(maxSeverity) < 0) { // severity could be less than previous max
 733                                                 maxSeverity = pageContainer.getRuleManager().getMaxSeverity(topics);
 734                                         }
 735                                         setToolTipText(tooltip.get());
 736                                 };
 737                                 listeners.add(listener);
 738                                 pageContainer.getRuleManager().addResultListener(topic, listener);
 739                         }
 740                 }
 741 
 742                 private void removeListeners() {
 743                         listeners.forEach(l -> pageContainer.getRuleManager().removeResultListener(l));
 744                 }
 745 
 746                 @Override
 747                 public void run() {
 748                         pageContainer.showResults(topics);
 749                 }
 750         }
 751 
 752         public static void addRuleResultAction(
 753                 Form form, IPageContainer pageContainer, Supplier<String> tooltip, String[] topics) {
 754                 if (topics == null || topics.length == 0 || !FlightRecorderUI.getDefault().isAnalysisEnabled()) {
 755                         return;
 756                 }
 757                 ImageDescriptor icon = getResultIcon(pageContainer.getRuleManager().getMaxSeverity(topics));
 758                 ShowResultAction resultAction = new ShowResultAction(Messages.RULES_SHOW_RESULTS_ACTION, IAction.AS_PUSH_BUTTON,
 759                                 icon, tooltip, pageContainer, topics);
 760                 resultAction.setId(RESULT_ACTION_ID);
 761                 form.getToolBarManager().appendToGroup(DataPageToolkit.FORM_TOOLBAR_PAGE_RESULTS, resultAction);
 762                 form.getToolBarManager().update(true);
 763                 form.addDisposeListener(e -> resultAction.removeListeners());
 764         }
 765 
 766         private static ImageDescriptor getResultIcon(Severity severity) {
 767                 switch (severity) {
 768                 case OK:
 769                         return ResultOverview.ICON_OK;
 770                 case INFO:
 771                         return ResultOverview.ICON_INFO;
 772                 case WARNING:
 773                         return ResultOverview.ICON_WARNING;
 774                 case NA:
 775                         return ResultOverview.ICON_NA;
 776                 }
 777                 return null;
 778         }
 779 
 780         /**
 781          * Return a disabled Action.
 782          *
 783          * @param text
 784          *            text to be displayed by the MenuItem, and represent it as it's id.
 785          * @return an Action containing the desired text, which will be disabled in a UI component.
 786          */
 787         public static IAction disabledAction(String text) {
 788                 IAction disabledAction = new Action(text) {
 789                         @Override
 790                         public boolean isEnabled() {
 791                                 return false;
 792                         }
 793                 };
 794                 disabledAction.setId(text);
 795                 return disabledAction;
 796         }
 797 
 798         public static FilterEditor buildFilterSelector(
 799                 Composite parent, IItemFilter filter, IItemCollection items, Supplier<Stream<SelectionStoreEntry>> selections,
 800                 Consumer<IItemFilter> onSelect, boolean hasBorder) {
 801                 Supplier<Collection<IAttribute<?>>> attributeSupplier = () -> getPersistableAttributes(
 802                                 getAttributes(filter != null ? items.apply(filter) : items)).collect(Collectors.toList());
 803 
 804                 AttributeValueProvider valueSupplier = new AttributeValueProvider() {
 805                         @Override
 806                         public <V> V defaultValue(ICanonicalAccessorFactory<V> attribute) {
 807                                 return findValueForFilter(items, attribute);
 808                         }
 809                 };
 810 
 811                 FilterEditor editor = new FilterEditor(parent, onSelect, filter, attributeSupplier, valueSupplier,
 812                                 TypeLabelProvider::getColorOrDefault, hasBorder ? SWT.BORDER : SWT.NONE);
 813 
 814                 MenuManager addFromSelectionPredicate = new MenuManager(Messages.FILTER_ADD_FROM_SELECTION);
 815                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addFromSelectionPredicate);
 816                 addFromSelectionPredicate.setRemoveAllWhenShown(true);
 817                 addFromSelectionPredicate.addMenuListener(new IMenuListener() {
 818 
 819                         @Override
 820                         public void menuAboutToShow(IMenuManager manager) {
 821                                 selections.get().forEach(entry -> {
 822                                         MenuManager selectionFlavors = new MenuManager(entry.getName());
 823                                         entry.getSelection().getFlavors(editor.getFilter(), items, null)
 824                                                         .filter(f -> f instanceof IFilterFlavor).forEach(flavor -> {
 825                                                                 selectionFlavors.add(new Action(flavor.getName()) {
 826                                                                         @Override
 827                                                                         public void run() {
 828                                                                                 editor.addRoot(((IFilterFlavor) flavor).getFilter());
 829                                                                         }
 830                                                                 });
 831                                                         });
 832                                         if (!selectionFlavors.isEmpty()) {
 833                                                 if (manager.find(Messages.FILTER_NO_SELECTION_AVAILABLE) != null) {
 834                                                         manager.remove(Messages.FILTER_NO_SELECTION_AVAILABLE);
 835                                                 }
 836                                                 manager.add(selectionFlavors);
 837                                         } else {
 838                                                 manager.add(disabledAction(Messages.FILTER_NO_SELECTION_AVAILABLE));
 839                                         }
 840                                 });
 841                         }
 842                 });
 843 
 844                 // FIXME: This could potentially move into the FilterEditor class
 845                 MenuManager addAttributeValuePredicate = new MenuManager(Messages.FILTER_ADD_FROM_ATTRIBUTE);
 846                 editor.getContextMenu().prependToGroup(MCContextMenuManager.GROUP_NEW, addAttributeValuePredicate);
 847                 addAttributeValuePredicate.setRemoveAllWhenShown(true);
 848                 addAttributeValuePredicate.addMenuListener(new IMenuListener() {
 849                         Collection<IAttribute<?>> attributes;
 850 
 851                         @Override
 852                         public void menuAboutToShow(IMenuManager manager) {
 853                                 if (attributes == null) {
 854                                         attributes = attributeSupplier.get();
 855                                 }
 856                                 if (!attributes.isEmpty()) {
 857                                         if (manager.find(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE) != null) {
 858                                                 manager.remove(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE);
 859                                         }
 860                                         attributes.stream().distinct().sorted((a1, a2) -> a1.getName().compareTo(a2.getName()))
 861                                                 .forEach(attr -> {
 862                                                         addAttributeValuePredicate.add(new Action(attr.getName()) {
 863                                                                 @Override
 864                                                                 public void run() {
 865                                                                         IItemFilter filter = createDefaultFilter(items, attr);
 866                                                                         editor.addRoot(filter);
 867                                                                 }
 868                                                         });
 869                                                 });
 870                                 } else {
 871                                         manager.add(disabledAction(Messages.FILTER_NO_ATTRIBUTE_AVAILABLE));
 872                                 }
 873 
 874                         }
 875                 });
 876                 return editor;
 877         }
 878 
 879         // FIXME: Move to some AttributeToolkit?
 880         private static Stream<IAttribute<?>> getAttributes(IItemCollection items) {
 881                 return ItemCollectionToolkit.stream(items).filter(IItemIterable::hasItems)
 882                                 .flatMap(is -> is.getType().getAttributes().stream());
 883         }
 884 
 885         public static Stream<IAttribute<?>> getPersistableAttributes(Stream<IAttribute<?>> attributes) {
 886                 // FIXME: Would like to always be able to persist a string representation of the attribute, because this is usable by filters.
 887 
 888                 // FIXME: Should we always include event type? Does it make any sense, except on the custom pages?
 889 
 890                 // FIXME: Transform both START_TIME and END_TIME to LIFETIME?
 891                 // FIXME: Add derived attributes, like a conversion of any THREAD or CLASS attribute? Thread group?
 892                 /*
 893                  * Make sure to do the conversions in the right order, so for example a stack trace can be
 894                  * converted to a top method, which then is converted to a method string.
 895                  */
 896                 return attributes.map(a -> a.equals(JfrAttributes.EVENT_THREAD) ? JdkAttributes.EVENT_THREAD_NAME : a)
 897                                 .flatMap(a -> a.equals(JfrAttributes.EVENT_STACKTRACE) ? Stream.of(JdkAttributes.STACK_TRACE_STRING,
 898                                                 JdkAttributes.STACK_TRACE_TOP_METHOD_STRING, JdkAttributes.STACK_TRACE_TOP_CLASS_STRING,
 899                                                 JdkAttributes.STACK_TRACE_TOP_PACKAGE, JdkAttributes.STACK_TRACE_BOTTOM_METHOD_STRING)
 900                                                 : Stream.of(a))
 901                                 .map(a -> a.equals(JdkAttributes.COMPILER_METHOD) ? JdkAttributes.COMPILER_METHOD_STRING : a)
 902                                 // FIXME: String or id?
 903                                 .map(a -> a.equals(JdkAttributes.REC_SETTING_FOR) ? JdkAttributes.REC_SETTING_FOR_NAME : a)
 904                                 .map(a -> a.equals(JdkAttributes.CLASS_DEFINING_CLASSLOADER)
 905                                                 ? JdkAttributes.CLASS_DEFINING_CLASSLOADER_STRING : a)
 906                                 .map(a -> a.equals(JdkAttributes.CLASS_INITIATING_CLASSLOADER)
 907                                                 ? JdkAttributes.CLASS_INITIATING_CLASSLOADER_STRING : a)
 908                                 .map(a -> a.equals(JdkAttributes.PARENT_CLASSLOADER)
 909                                                 ? JdkAttributes.PARENT_CLASSLOADER_STRING : a)
 910                                 .map(a -> a.equals(JdkAttributes.CLASSLOADER)
 911                                                 ? JdkAttributes.CLASSLOADER_STRING : a)
 912                                 .filter(a -> a.equals(JfrAttributes.EVENT_TYPE) || (a.getContentType() instanceof RangeContentType)
 913                                                 || (a.getContentType().getPersister() != null))
 914                                 .distinct();
 915         }
 916 
 917         /**
 918          * Returns a value for attribute, firstly by trying to find one in the items, secondly by
 919          * creating a default value for some known content types. Returns null if the first two cases
 920          * fail.
 921          *
 922          * @param items
 923          * @param attribute
 924          * @return a value of type V, or null
 925          */
 926         @SuppressWarnings("unchecked")
 927         private static <V> V findValueForFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 928                 IItem firstItem = ItemCollectionToolkit.stream(items).filter(is -> is.getType().hasAttribute(attribute))
 929                                 .flatMap(ItemIterableToolkit::stream)
 930                                 .filter(i -> ((IMemberAccessor<V, IItem>) attribute.getAccessor(i.getType())).getMember(i) != null)
 931                                 .findFirst().orElse(null);
 932                 if (firstItem != null) {
 933                         IMemberAccessor<V, IItem> accessor = (IMemberAccessor<V, IItem>) attribute.getAccessor(firstItem.getType());
 934                         return accessor.getMember(firstItem);
 935                 }
 936                 if (UnitLookup.PLAIN_TEXT.equals(attribute.getContentType())) {
 937                         return (V) ""; //$NON-NLS-1$
 938                 }
 939                 if (attribute.getContentType() instanceof KindOfQuantity<?>) {
 940                         return (V) ((KindOfQuantity<?>) attribute.getContentType()).getDefaultUnit().quantity(0);
 941                 }
 942                 return null;
 943         }
 944 
 945         /**
 946          * Returns an default filter for attribute, which might be an equals filter, hasAttribute
 947          * filter, or type filter, depending on the attribute and the contents of the items.
 948          *
 949          * @param items
 950          * @param attribute
 951          * @return a filter
 952          */
 953         // FIXME: Should move to FilterEditor, or some subclass/specialization?
 954         private static <V> IItemFilter createDefaultFilter(IItemCollection items, ICanonicalAccessorFactory<V> attribute) {
 955                 V value = findValueForFilter(items, attribute);
 956                 if (value == null) {
 957                         return ItemFilters.hasAttribute(attribute);
 958                 } else if (attribute.equals(JfrAttributes.EVENT_TYPE)) {
 959                         return ItemFilters.type(((IType<?>) value).getIdentifier());
 960                 }
 961                 return ItemFilters.equals(attribute, value);
 962         }
 963 
 964         public static void addRenameAction(Form form, IPageContainer editor) {
 965                 form.getMenuManager().add(new Action(Messages.PAGE_RENAME_MENU_ACTION) {
 966                         @Override
 967                         public void run() {
 968                                 InputDialog dialog = new InputDialog(form.getShell(), Messages.PAGE_RENAME_DIALOG_TITLE,
 969                                                 Messages.PAGE_RENAME_DIALOG_MESSAGE, form.getText(), null);
 970                                 if (dialog.open() == Window.OK) {
 971                                         form.setText(dialog.getValue());
 972                                         editor.currentPageRefresh();
 973                                 }
 974                         }
 975                 });
 976         }
 977 
 978         public static void addIconChangeAction(Form form, IPageContainer editor, Consumer<Image> newIconConsumer) {
 979                 form.getMenuManager().add(new Action(Messages.PAGE_CHANGE_ICON_MENU_ACTION) {
 980                         @Override
 981                         public void run() {
 982                                 WizardDialog dialog = new WizardDialog(form.getShell(),
 983                                                 new IconChangeWizard(form.getImage(), newIconConsumer));
 984                                 dialog.open();
 985                                 editor.currentPageRefresh();
 986                         }
 987                 });
 988         }
 989 
 990         private static class IconChangeWizard extends Wizard {
 991 
 992                 private final Image currentImage;
 993                 private final Consumer<Image> imageConsumer;
 994                 private Label imageLabel;
 995 
 996                 public IconChangeWizard(Image currentImage, Consumer<Image> imageConsumer) {
 997                         setWindowTitle(Messages.PAGE_CHANGE_ICON_WIZARD_TITLE);
 998                         this.currentImage = currentImage;
 999                         this.imageConsumer = imageConsumer;
1000                 }
1001 
1002                 @Override
1003                 public void addPages() {
1004                         addPage(new WizardPage(Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE) {
1005 
1006                                 @Override
1007                                 public String getTitle() {
1008                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_TITLE;
1009                                 }
1010 
1011                                 @Override
1012                                 public String getDescription() {
1013                                         return Messages.PAGE_CHANGE_ICON_WIZARD_PAGE_DESC;
1014                                 }
1015 
1016                                 @Override
1017                                 public void createControl(Composite parent) {
1018                                         Composite container = new Composite(parent, SWT.NONE);
1019                                         GridLayout layout = new GridLayout(1, false);
1020                                         container.setLayout(layout);
1021 
1022                                         Button button = new Button(container, SWT.NONE);
1023                                         button.setText(Messages.PAGE_CHANGE_ICON_CHOOSE_IMAGE_FILE);
1024 
1025                                         button.addSelectionListener(new SelectionAdapter() {
1026                                                 @Override
1027                                                 public void widgetSelected(SelectionEvent e) {
1028                                                         chooseImageFileDialog();
1029                                                 }
1030                                         });
1031 
1032                                         if (currentImage != null) {
1033                                                 new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_CURRENT_ICON);
1034                                                 new Label(container, SWT.BORDER).setImage(currentImage);
1035                                         }
1036                                         new Label(container, SWT.NONE).setText(Messages.PAGE_CHANGE_ICON_NEW_ICON_PREVIEW);
1037                                         imageLabel = new Label(container, SWT.BORDER);
1038                                         GridData gd = new GridData(16, 16);
1039                                         imageLabel.setLayoutData(gd);
1040 
1041                                         setControl(container);
1042                                 }
1043 
1044                                 private void chooseImageFileDialog() {
1045                                         FileDialog fileDialog = new FileDialog(getShell(), SWT.OPEN);
1046                                         String[] filterNames = new String[] {"Image Files", "All Files (*)"}; //$NON-NLS-1$ //$NON-NLS-2$
1047                                         String[] filterExtensions = new String[] {"*.gif;*.png;*.xpm;*.jpg;*.jpeg;*.tiff", "*"}; //$NON-NLS-1$ //$NON-NLS-2$
1048                                         fileDialog.setFilterNames(filterNames);
1049                                         fileDialog.setFilterExtensions(filterExtensions);
1050                                         String filename = fileDialog.open();
1051                                         if (filename == null) {
1052                                                 // Dialog was cancelled. Bail out early to avoid handling that case later. Premature?
1053                                                 return;
1054                                         }
1055                                         try (InputStream fis = new FileInputStream(filename)) {
1056                                                 ImageData imageData = new ImageData(fis);
1057                                                 // Validate image data
1058                                                 if (imageData.width != 16 || imageData.height != 16) {
1059                                                         imageData = resizeImage(imageData, 16, 16);
1060                                                 }
1061                                                 DisplayToolkit.dispose(imageLabel.getImage());
1062                                                 imageLabel.setImage(new Image(getShell().getDisplay(), imageData));
1063                                                 imageLabel.getParent().layout();
1064                                                 setPageComplete(isPageComplete());
1065                                         } catch (Exception e) {
1066                                                 // FIXME: Add proper logging
1067                                                 e.printStackTrace();
1068                                         }
1069                                 }
1070 
1071                                 private ImageData resizeImage(ImageData imageData, int width, int height) {
1072                                         Image original = ImageDescriptor.createFromImageData(imageData).createImage();
1073                                         Image scaled = new Image(Display.getDefault(), width, height);
1074                                         GC gc = new GC(scaled);
1075                                         gc.setAntialias(SWT.ON);
1076                                         gc.setInterpolation(SWT.HIGH);
1077                                         gc.drawImage(original, 0, 0, imageData.width, imageData.height, 0, 0, width, height);
1078                                         gc.dispose();
1079                                         original.dispose();
1080                                         ImageData scaledData = scaled.getImageData();
1081                                         scaled.dispose();
1082                                         return scaledData;
1083                                 }
1084 
1085                                 @Override
1086                                 public boolean isPageComplete() {
1087                                         return imageLabel.getImage() != null;
1088                                 }
1089 
1090                         });
1091                 }
1092 
1093                 @Override
1094                 public boolean performFinish() {
1095                         imageConsumer.accept(imageLabel.getImage());
1096                         DisplayToolkit.dispose(currentImage);
1097                         return true;
1098                 }
1099 
1100                 @Override
1101                 public boolean performCancel() {
1102                         DisplayToolkit.dispose(imageLabel.getImage());
1103                         return true;
1104                 }
1105 
1106         }
1107 
1108         public static ItemList createSimpleItemList(
1109                 Composite parent, ItemListBuilder listBuilder, IPageContainer pageContainer, TableSettings tableSettings,
1110                 String selectionName) {
1111 
1112                 ItemList list = listBuilder.build(parent, tableSettings);
1113                 ColumnViewer viewer = list.getManager().getViewer();
1114                 MCContextMenuManager mm = MCContextMenuManager.create(viewer.getControl());
1115                 ColumnMenusFactory.addDefaultMenus(list.getManager(), mm);
1116                 viewer.addSelectionChangedListener(
1117                                 e -> pageContainer.showSelection(ItemCollectionToolkit.build(list.getSelection().get())));
1118 
1119                 if (selectionName != null) {
1120                         SelectionStoreActionToolkit.addSelectionStoreActions(pageContainer.getSelectionStore(), list, selectionName,
1121                                         mm);
1122                 }
1123 
1124                 return list;
1125         }
1126 
1127         public static void addTabItem(CTabFolder tabFolder, Control section, String name) {
1128                 CTabItem tabItem = new CTabItem(tabFolder, SWT.NONE);
1129                 tabItem.setControl(section);
1130                 tabItem.setText(name);
1131         }
1132 
1133         public static TypeFilterBuilder buildEventTypeTree(
1134                 Composite parent, FormToolkit toolkit, Runnable onChange, boolean checkbox) {
1135                 // TODO: Make more accessible.
1136                 // TODO: Add support for storing the expansion state in a memento.
1137                 // TODO: Add input from selection store, output to selection store
1138                 // TODO: Add toolbar for choosing tree or checkbox tree.
1139                 Composite treeComposite = new Composite(parent, SWT.NONE);
1140                 treeComposite.setLayout(new GridLayout());
1141                 toolkit.adapt(treeComposite);
1142                 Label caption = toolkit.createLabel(treeComposite, Messages.EVENT_TYPE_TREE_TITLE);
1143                 caption.setFont(JFaceResources.getFontRegistry().getBold(JFaceResources.DEFAULT_FONT));
1144                 caption.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
1145 
1146                 TypeFilterBuilder typeFilterTree = new TypeFilterBuilder(treeComposite, onChange, checkbox);
1147 
1148                 typeFilterTree.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
1149                 return typeFilterTree;
1150         }
1151 
1152         public static boolean isTypeWithThreadAndDuration(IType<?> type) {
1153                 return JfrAttributes.EVENT_THREAD.getAccessor(type) != null
1154                                 && JfrAttributes.START_TIME.getAccessor(type) != JfrAttributes.END_TIME.getAccessor(type);
1155         }
1156 
1157         public static void addPage(Set<IType<?>> selectedTypes) {
1158                 PageManager pm = FlightRecorderUI.getDefault().getPageManager();
1159                 pm.makeRoot(pm.createPage(ItemHandlerPage.Factory.class, new ItemHandlerUiStandIn(selectedTypes)));
1160         }
1161 }