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;
  34 
  35 import java.text.MessageFormat;
  36 import java.util.ArrayList;
  37 import java.util.Arrays;
  38 import java.util.Collection;
  39 import java.util.Collections;
  40 import java.util.Comparator;
  41 import java.util.HashSet;
  42 import java.util.Iterator;
  43 import java.util.LinkedList;
  44 import java.util.List;
  45 import java.util.Objects;
  46 import java.util.Set;
  47 import java.util.concurrent.CompletableFuture;
  48 import java.util.function.Consumer;
  49 import java.util.function.Function;
  50 import java.util.function.Predicate;
  51 import java.util.logging.Level;
  52 import java.util.stream.Collectors;
  53 import java.util.stream.Stream;
  54 
  55 import org.eclipse.jface.action.IAction;
  56 import org.eclipse.jface.resource.ImageDescriptor;
  57 import org.eclipse.jface.viewers.ArrayContentProvider;
  58 import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
  59 import org.eclipse.jface.viewers.ISelection;
  60 import org.eclipse.jface.viewers.IStructuredSelection;
  61 import org.eclipse.jface.viewers.TableViewer;
  62 import org.eclipse.osgi.util.NLS;
  63 import org.eclipse.swt.SWT;
  64 import org.eclipse.swt.graphics.Image;
  65 import org.eclipse.swt.widgets.Composite;
  66 import org.eclipse.swt.widgets.Control;
  67 import org.eclipse.swt.widgets.Display;
  68 import org.eclipse.ui.IWorkbenchPart;
  69 import org.eclipse.ui.PlatformUI;
  70 import org.eclipse.ui.part.Page;
  71 import org.eclipse.ui.views.properties.IPropertySheetPage;
  72 import org.openjdk.jmc.common.IDescribable;
  73 import org.openjdk.jmc.common.IDisplayable;
  74 import org.openjdk.jmc.common.IState;
  75 import org.openjdk.jmc.common.collection.IteratorToolkit;
  76 import org.openjdk.jmc.common.item.Aggregators;
  77 import org.openjdk.jmc.common.item.IAttribute;
  78 import org.openjdk.jmc.common.item.IItem;
  79 import org.openjdk.jmc.common.item.IItemCollection;
  80 import org.openjdk.jmc.common.item.IItemFilter;
  81 import org.openjdk.jmc.common.item.IItemIterable;
  82 import org.openjdk.jmc.common.item.IMemberAccessor;
  83 import org.openjdk.jmc.common.item.IType;
  84 import org.openjdk.jmc.common.unit.ContentType;
  85 import org.openjdk.jmc.common.unit.IQuantity;
  86 import org.openjdk.jmc.common.unit.IRange;
  87 import org.openjdk.jmc.common.unit.KindOfQuantity;
  88 import org.openjdk.jmc.common.unit.QuantitiesToolkit;
  89 import org.openjdk.jmc.common.unit.QuantityRange;
  90 import org.openjdk.jmc.common.unit.RangeContentType;
  91 import org.openjdk.jmc.common.unit.UnitLookup;
  92 import org.openjdk.jmc.common.util.TypeHandling;
  93 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  94 import org.openjdk.jmc.flightrecorder.ui.common.DataPageToolkit;
  95 import org.openjdk.jmc.flightrecorder.ui.common.ImageConstants;
  96 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
  97 import org.openjdk.jmc.flightrecorder.ui.preferences.PreferenceKeys;
  98 import org.openjdk.jmc.flightrecorder.ui.selection.FlavoredSelectionBase;
  99 import org.openjdk.jmc.flightrecorder.ui.selection.IFilterFlavor;
 100 import org.openjdk.jmc.flightrecorder.ui.selection.IFlavoredSelection;
 101 import org.openjdk.jmc.flightrecorder.ui.selection.IItemStreamFlavor;
 102 import org.openjdk.jmc.flightrecorder.ui.selection.IPropertyFlavor;
 103 import org.openjdk.jmc.flightrecorder.ui.selection.ItemBackedSelection;
 104 import org.openjdk.jmc.ui.TypeAppearance;
 105 import org.openjdk.jmc.ui.UIPlugin;
 106 import org.openjdk.jmc.ui.accessibility.FocusTracker;
 107 import org.openjdk.jmc.ui.column.ColumnBuilder;
 108 import org.openjdk.jmc.ui.column.ColumnManager;
 109 import org.openjdk.jmc.ui.column.ColumnMenusFactory;
 110 import org.openjdk.jmc.ui.column.IColumn;
 111 import org.openjdk.jmc.ui.column.TableSettings;
 112 import org.openjdk.jmc.ui.column.TableSettings.ColumnSettings;
 113 import org.openjdk.jmc.ui.common.util.AdapterUtil;
 114 import org.openjdk.jmc.ui.handlers.ActionToolkit;
 115 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
 116 import org.openjdk.jmc.ui.misc.DisplayToolkit;
 117 import org.openjdk.jmc.ui.misc.TypedLabelProvider;
 118 
 119 // FIXME: fields - units - filters - icons etc. should be handled more properly
 120 public class JfrPropertySheet extends Page implements IPropertySheetPage {
 121 
 122         private static final ImageDescriptor NEW_PAGE_ICON = FlightRecorderUI.getDefault()
 123                         .getMCImageDescriptor(ImageConstants.ICON_NEW_PAGE);
 124         private static final String HELP_CONTEXT_ID = FlightRecorderUI.PLUGIN_ID + ".JfrPropertiesView"; //$NON-NLS-1$
 125         private static final Object TOO_MANY_VALUES = new Object();
 126         private static final PropertySheetRow CALCULATING = new PropertySheetRow(null, null);
 127         private static Set<IType<?>> selectedTypes = Collections.emptySet();
 128         private static class PropertySheetRowSelection extends FlavoredSelectionBase {
 129 
 130                 private final PropertySheetRow row;
 131 
 132                 PropertySheetRowSelection(PropertySheetRow row) {
 133                         super(MessageFormat.format(Messages.JFR_PROPERTIES_PROPERTY_SELECTION, row.attribute.getName()));
 134                         this.row = row;
 135                 }
 136 
 137                 @Override
 138                 public Stream<IItemStreamFlavor> getFlavors(
 139                         IItemFilter dstFilter, IItemCollection items, List<IAttribute<?>> dstAttributes) {
 140                         /*
 141                          * FIXME: Is this the desired behavior? Discuss and change if necessary.
 142                          * 
 143                          * This most likely need more thought and discussion, but the implemented order of
 144                          * flavors is currently:
 145                          * 
 146                          * For chart selections:
 147                          * 
 148                          * 1: The selected events if any of them appear on the destination page
 149                          * 
 150                          * 2: All events on the destination page in the selected range (if a range was selected)
 151                          * 
 152                          * 3-n: All events on the destination page filtered on any of the attributes common to
 153                          * all selected events (excluding the range attribute if a range was selected)
 154                          * 
 155                          * For histogram and list selections:
 156                          * 
 157                          * 1: The selected events if any of them appear on the destination page
 158                          * 
 159                          * 2-n: All events on the destination page filtered on any of the attributes common to
 160                          * all selected events (all will at least have (endTime))
 161                          * 
 162                          * For properties view selections:
 163                          * 
 164                          * 1: All events on the destination page filtered on the selected attribute:value if
 165                          * they all have the selected attribute
 166                          * 
 167                          * 2: All events on the destination page filtered on the selected value if they all have
 168                          * an attribute with the same content type
 169                          * 
 170                          * 3: All events on the destination page filtered on all common attributes with values
 171                          * from all events filtered on the selected attribute:value (see example)
 172                          * 
 173                          * 4: All events on the destination page filtered on the selected attribute:value if
 174                          * there are any events with the attribute (and the attribute is not common to all
 175                          * events in which case this flavor has already been added in (1))
 176                          * 
 177                          * Example of properties view selections (3):
 178                          * 
 179                          * ECID:1-2-3-4 was selected and the user navigates to Java Application. All events on
 180                          * Java Application share (thread) and (endTime), so all events on the page are filtered
 181                          * on those properties. The values to include are collected from all events with the
 182                          * ECID attribute having value 1-2-3-4. The threads will be put in a set, the timestamps
 183                          * will form a range.
 184                          */
 185                         IItemCollection filteredDstItems = ItemCollectionToolkit.filterIfNotNull(items, dstFilter);
 186                         IPropertyFlavor relatedFilterFlavor = IPropertyFlavor.build(row.attribute, row.value, filteredDstItems);
 187                         LinkedList<IItemStreamFlavor> flavors = new LinkedList<>();
 188 
 189                         boolean anyRelatedOnDst = relatedFilterFlavor.evaluate().hasItems();
 190                         IPropertyFlavor selectedPropertyFlavor = IPropertyFlavor.build(row.attribute, row.value, items);
 191                         if (anyRelatedOnDst) {
 192                                 // prio1(a): Items related to the selected attribute if there are any
 193                                 flavors.add(selectedPropertyFlavor);
 194                                 selectedPropertyFlavor = null;
 195                         }
 196                         IItemCollection itemsRelatedToSelection = items.apply(relatedFilterFlavor.getFilter());
 197                         if (dstAttributes == null || dstAttributes.isEmpty()) {
 198                                 dstAttributes = commonAttributes(filteredDstItems.iterator()).collect(Collectors.toList());
 199                         }
 200                         Iterator<IAttribute<?>> commonDstAttr = dstAttributes.iterator();
 201                         List<IPropertyFlavor> relatedProperties = new ArrayList<>();
 202                         while (commonDstAttr.hasNext()) {
 203                                 IAttribute<?> dstAttribute = commonDstAttr.next();
 204                                 if (!dstAttribute.equals(JfrAttributes.EVENT_TYPE)
 205                                                 && (!(dstAttribute.getContentType() instanceof KindOfQuantity)
 206                                                                 || dstAttribute.equals(JfrAttributes.END_TIME))) {
 207                                         // FIXME: Collect type or quantity values?
 208                                         if (dstAttribute.equals(row.attribute)) {
 209                                                 if (!anyRelatedOnDst && selectedPropertyFlavor != null) {
 210                                                         // prio1(b): Related to the selected attribute even though it's empty, since the attribute is shared by all
 211                                                         flavors.push(selectedPropertyFlavor);
 212                                                         selectedPropertyFlavor = null;
 213                                                 }
 214                                                 relatedProperties = null;
 215                                         } else if (!dstAttribute.equals(row.attribute)
 216                                                         && dstAttribute.getContentType().equals(row.attribute.getContentType())) {
 217                                                 // prio2: Destination items with an attribute of the selected content type and which equals the selected value
 218                                                 flavors.add(IPropertyFlavor.build(dstAttribute, row.value, items));
 219                                         }
 220                                         if (relatedProperties != null) {
 221                                                 // Collect values from items related to selection (only items of types that has the attribute), and add as filter
 222                                                 PropertySheetRow av = buildProperty(dstAttribute,
 223                                                                 ItemCollectionToolkit.stream(itemsRelatedToSelection)
 224                                                                                 .filter(is -> dstAttribute.getAccessor(is.getType()) != null).iterator(),
 225                                                                 Integer.MAX_VALUE);
 226                                                 if (av != null) {
 227                                                         relatedProperties.add(IPropertyFlavor.build(av.attribute, av.value, items));
 228                                                 }
 229                                         }
 230                                 }
 231                         }
 232                         if (relatedProperties != null) {
 233                                 if (relatedProperties.size() > 1) {
 234                                         // prio3: Destination items with properties shared with the items related to the selection
 235                                         flavors.add(IPropertyFlavor.combine(relatedProperties::stream, items));
 236                                 }
 237 
 238                                 // FIXME: Combinations with for example two properties if there are three properties in total shared?
 239 
 240                                 // prio4: Destination items with one property shared with the items related to the selection
 241                                 flavors.addAll(relatedProperties);
 242                         }
 243                         if (selectedPropertyFlavor != null) {
 244                                 // prio4: Items related to the selected attribute even if there aren't any
 245                                 flavors.add(selectedPropertyFlavor);
 246                         }
 247                         return flavors.stream();
 248                 }
 249         }
 250 
 251         static class PropertySheetRow {
 252                 final IAttribute<?> attribute;
 253                 final Object value;
 254 
 255                 PropertySheetRow(IAttribute<?> attribute, Object value) {
 256                         this.attribute = attribute;
 257                         this.value = value;
 258                 }
 259 
 260                 public IAttribute<?> getAttribute() {
 261                         return attribute;
 262                 }
 263 
 264                 public Object getValue() {
 265                         return value;
 266                 }
 267 
 268         }
 269 
 270         private static final IColumn FIELD_COLUMN = new ColumnBuilder(Messages.JFR_PROPERTY_SHEET_FIELD, "field", //$NON-NLS-1$
 271                         new TypedLabelProvider<PropertySheetRow>(PropertySheetRow.class) {
 272 
 273                                 @Override
 274                                 protected String getTextTyped(PropertySheetRow p) {
 275                                         return p.attribute == null ? "" : p.attribute.getName(); //$NON-NLS-1$
 276                                 };
 277 
 278                                 @Override
 279                                 protected String getToolTipTextTyped(PropertySheetRow p) {
 280                                         // FIXME: This is duplicated in EventBrowserPage, where we also create a tooltip for an attribute.
 281                                         return p.attribute == null ? "" //$NON-NLS-1$
 282                                                         : NLS.bind(Messages.ATTRIBUTE_ID_LABEL, p.attribute.getIdentifier())
 283                                                                         + System.getProperty("line.separator") //$NON-NLS-1$
 284                                                                         + NLS.bind(Messages.ATTRIBUTE_DESCRIPTION_LABEL, p.attribute.getDescription());
 285                                 };
 286 
 287                                 @Override
 288                                 protected Image getImageTyped(PropertySheetRow p) {
 289                                         if (p.attribute != null) {
 290                                                 Image icon = TypeAppearance.getImage(p.attribute.getContentType().getIdentifier());
 291                                                 return icon == null ? UIPlugin.getDefault().getImage(UIPlugin.ICON_PROPERTY_OBJECT) : icon;
 292                                         }
 293                                         return null;
 294                                 };
 295                         }).build();
 296 
 297         private static final IColumn VALUE_COLUMN = new ColumnBuilder(Messages.JFR_PROPERTY_SHEET_VALUE, "value", //$NON-NLS-1$
 298                         new TypedLabelProvider<PropertySheetRow>(PropertySheetRow.class) {
 299                                 @Override
 300                                 protected String getTextTyped(PropertySheetRow p) {
 301                                         Object value = p.getValue();
 302                                         if (p == CALCULATING) {
 303                                                 return Messages.JFR_PROPERTIES_CALCULATING;
 304                                         } else if (value == TOO_MANY_VALUES) {
 305                                                 return Messages.JFR_PROPERTIES_TOO_MANY_VALUES;
 306                                         }
 307                                         updateSelectedTypes(value);
 308                                         return getValueString(value);
 309                                 };
 310 
 311                                 // FIXME: Merge with TypeHandling.getValueString
 312                                 @SuppressWarnings("unchecked")
 313                                 private String getValueString(Object value) {
 314                                         if (value instanceof IItemCollection) {
 315                                                 return itemCollectionDescription((IItemCollection) value);
 316                                         } else if (value instanceof IDescribable) {
 317                                                 return ((IDescribable) value).getName();
 318                                         } else if (value instanceof IDescribable[] && ((IDescribable[]) value).length > 0) {
 319                                                 IDescribable[] values = ((IDescribable[]) value);
 320                                                 return "[" + values[0].getName() + " ... " //$NON-NLS-1$ //$NON-NLS-2$
 321                                                                 + values[values.length - 1].getName() + "]"; //$NON-NLS-1$
 322                                         } else if (value instanceof Object[]) {
 323                                                 return limitedDeepToString((Object[]) value, this::getValueString);
 324                                         } else if (value instanceof Collection<?>) {
 325                                                 selectedTypes = new HashSet<IType<?>>((Collection<IType<?>>) value)
 326                                                         .stream().collect(Collectors.toSet());
 327                                                 return limitedDeepToString(((Collection<?>) value).toArray(), this::getValueString);
 328                                         }
 329                                         return TypeHandling.getValueString(value);
 330                                 }
 331 
 332                                 @Override
 333                                 protected String getToolTipTextTyped(PropertySheetRow p) {
 334                                         Object value = p.getValue();
 335                                         if (value instanceof IQuantity) {
 336                                                 return TypeHandling.getNumericString(((IQuantity) value).numberValue());
 337                                         }
 338                                         return JfrPropertySheet.getVerboseString(value);
 339                                 };
 340                         }).build();
 341 
 342         @SuppressWarnings("unchecked")
 343         private static void updateSelectedTypes(Object value) {
 344                 if (value instanceof IType<?>) {
 345                         selectedTypes = new HashSet<IType<?>>();
 346                         selectedTypes.add((IType<?>) value);
 347                 }
 348                 else if (value instanceof Collection<?>) {
 349                         selectedTypes = new HashSet<IType<?>>((Collection<IType<?>>) value)
 350                                         .stream().collect(Collectors.toSet());
 351                 }
 352         }
 353 
 354         private static String limitedDeepToString(Object[] array, Function<Object, String> valueToStringProvider) {
 355                 return limitedDeepToString(array, new StringBuilder(), true, valueToStringProvider);
 356         }
 357 
 358         private static String limitedDeepToString(
 359                 Object[] array, StringBuilder builder, boolean isRootArray, Function<Object, String> valueToStringProvider) {
 360                 int maxCharacters = FlightRecorderUI.getDefault().getPreferenceStore()
 361                                 .getInt(PreferenceKeys.PROPERTY_MAXIMUM_PROPERTIES_ARRAY_STRING_SIZE);
 362                 int omitted = 0;
 363                 builder.append('[');
 364                 for (int i = 0; i < array.length; i++) {
 365                         Object element = array[i];
 366                         if (element != null && element.getClass().isArray()) {
 367                                 limitedDeepToString((Object[]) element, builder, false, valueToStringProvider);
 368                         } else {
 369                                 builder.append(valueToStringProvider.apply(element));
 370                         }
 371                         if ((i < (array.length - 1)) && builder.length() < maxCharacters) {
 372                                 builder.append(',');
 373                                 builder.append(' ');
 374                         }
 375                         if (isRootArray && (builder.length() > maxCharacters)) {
 376                                 builder.setLength(maxCharacters);
 377                                 builder.append(Messages.JFR_PROPERTIES_INSERTED_ELLIPSIS);
 378                                 omitted = (array.length - 1) - i;
 379                                 break;
 380                         }
 381                 }
 382                 if (isRootArray && omitted > 0) {
 383                         builder.append(' ');
 384                         if (omitted > 1) {
 385                                 builder.append(MessageFormat.format(Messages.JFR_PROPERTIES_ARRAY_WITH_OMITTED_ELEMENTS, omitted));
 386                         } else {
 387                                 builder.append(Messages.JFR_PROPERTIES_ARRAY_WITH_OMITTED_ELEMENT);
 388                         }
 389                 }
 390                 builder.append(']');
 391                 return builder.toString();
 392         }
 393 
 394         private static final IColumn VERBOSE_VALUE_COLUMN = new ColumnBuilder(Messages.JFR_PROPERTY_SHEET_VERBOSE_VALUE,
 395                         "verboseValue", //$NON-NLS-1$
 396                         new TypedLabelProvider<PropertySheetRow>(PropertySheetRow.class) {
 397                                 @Override
 398                                 protected String getTextTyped(PropertySheetRow p) {
 399                                         Object value = p.getValue();
 400                                         if (p == CALCULATING) {
 401                                                 return Messages.JFR_PROPERTIES_CALCULATING;
 402                                         } else if (value == TOO_MANY_VALUES) {
 403                                                 return Messages.JFR_PROPERTIES_TOO_MANY_VALUES;
 404                                         }
 405                                         return JfrPropertySheet.getVerboseString(value);
 406                                 };
 407 
 408                                 @Override
 409                                 protected String getToolTipTextTyped(PropertySheetRow p) {
 410                                         return getTextTyped(p);
 411                                 };
 412 
 413                         }).build();
 414 
 415         // FIXME: Merge with TypeHandling.getVerboseString
 416         private static String getVerboseString(Object value) {
 417                 if (value instanceof IDisplayable) {
 418                         return ((IDisplayable) value).displayUsing(IDisplayable.VERBOSE);
 419                 } else if (value instanceof IItemCollection) {
 420                         return ItemCollectionToolkit.getDescription(((IItemCollection) value));
 421                 } else if (value instanceof IDescribable) {
 422                         return ((IDescribable) value).getDescription();
 423                 } else if (value instanceof IDescribable[] && ((IDescribable[]) value).length > 0) {
 424                         IDescribable[] values = ((IDescribable[]) value);
 425                         return "[" + values[0].getDescription() + " ... " //$NON-NLS-1$ //$NON-NLS-2$
 426                                         + values[values.length - 1].getDescription() + "]"; //$NON-NLS-1$
 427                 } else if (value instanceof Object[]) {
 428                         return limitedDeepToString((Object[]) value, JfrPropertySheet::getVerboseString);
 429                 } else if (value instanceof Collection) {
 430                         return limitedDeepToString(((Collection<?>) value).toArray(), JfrPropertySheet::getVerboseString);
 431                 }
 432 
 433                 return TypeHandling.getVerboseString(value);
 434         }
 435 
 436         private TableViewer viewer;
 437         private final IPageContainer controller;
 438         private CompletableFuture<Void> viewerUpdater;
 439 
 440         JfrPropertySheet(IPageContainer controller) {
 441                 this.controller = controller;
 442         }
 443 
 444         @Override
 445         public void createControl(Composite parent) {
 446                 viewer = new TableViewer(parent, SWT.MULTI | SWT.FULL_SELECTION);
 447                 viewer.setContentProvider(ArrayContentProvider.getInstance());
 448                 // FIXME: Should we keep a state for the properties view?
 449                 ColumnManager manager = ColumnManager.build(viewer,
 450                                 Arrays.asList(FIELD_COLUMN, VALUE_COLUMN, VERBOSE_VALUE_COLUMN), getTableSettings(null));
 451                 MCContextMenuManager mm = MCContextMenuManager.create(viewer.getControl());
 452                 ColumnMenusFactory.addDefaultMenus(manager, mm);
 453                 Function<Consumer<IFlavoredSelection>, Function<List<PropertySheetRow>, Runnable>> actionProvider = flavorConsumer -> selected -> {
 454                         if (selected.size() == 1 && selected.get(0).value != TOO_MANY_VALUES) {
 455                                 if (selected.get(0).attribute != null) {
 456                                         return () -> flavorConsumer.accept(new PropertySheetRowSelection(selected.get(0)));
 457                                 } else if (selected.get(0).value instanceof IItemCollection) {
 458                                         IItemCollection items = (IItemCollection) selected.get(0).value;
 459                                         String selectionName = itemCollectionDescription(items);
 460                                         return () -> flavorConsumer.accept(new ItemBackedSelection(items, selectionName));
 461                                 }
 462                         }
 463                         return null;
 464                 };
 465                 // FIXME: Break out to other place where these actions are added to menus
 466                 IAction addPageAction = ActionToolkit.action(() -> DataPageToolkit.addPage(selectedTypes),
 467                                 Messages.EventBrowserPage_NEW_PAGE_USING_TYPES_ACTION, NEW_PAGE_ICON);
 468                 mm.appendToGroup(MCContextMenuManager.GROUP_NEW, addPageAction);
 469                 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT,
 470                                 ActionToolkit.forListSelection(viewer, Messages.STORE_SELECTION_ACTION, false,
 471                                                 actionProvider.apply(controller.getSelectionStore()::addSelection)));
 472                 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT,
 473                                 ActionToolkit.forListSelection(viewer, Messages.STORE_AND_ACTIVATE_SELECTION_ACTION, false,
 474                                                 actionProvider.apply(controller.getSelectionStore()::addAndSetAsCurrentSelection)));
 475                 ColumnViewerToolTipSupport.enableFor(viewer);
 476                 PlatformUI.getWorkbench().getHelpSystem().setHelp(viewer.getControl(), HELP_CONTEXT_ID);
 477 
 478                 if (UIPlugin.getDefault().getAccessibilityMode()) {
 479                         FocusTracker.enableFocusTracking(viewer.getTable());
 480                 }
 481         }
 482 
 483         private static TableSettings getTableSettings(IState state) {
 484                 if (state == null) {
 485                         return new TableSettings(null,
 486                                         Arrays.asList(new ColumnSettings(FIELD_COLUMN.getId(), false, 120, null),
 487                                                         new ColumnSettings(VALUE_COLUMN.getId(), false, 120, null),
 488                                                         new ColumnSettings(VERBOSE_VALUE_COLUMN.getId(), true, 400, null)));
 489                 } else {
 490                         return new TableSettings(state);
 491                 }
 492         }
 493 
 494         private static String itemCollectionDescription(IItemCollection items) {
 495                 IQuantity count = items.getAggregate(Aggregators.count());
 496                 return NLS.bind(Messages.JFR_PROPERTY_SHEET_EVENTS, count == null ? 0 : count.displayUsing(IDisplayable.AUTO));
 497         }
 498 
 499         @Override
 500         public void selectionChanged(IWorkbenchPart part, ISelection selection) {
 501                 if (selection instanceof IStructuredSelection) {
 502                         Object first = ((IStructuredSelection) selection).getFirstElement();
 503                         IItemCollection items = AdapterUtil.getAdapter(first, IItemCollection.class);
 504                         if (items != null) {
 505                                 show(items);
 506                         }
 507                 }
 508         }
 509 
 510         private void show(IItemCollection items) {
 511                 if (viewerUpdater != null) {
 512                         viewerUpdater.complete(null);
 513                 }
 514                 CompletableFuture<PropertySheetRow[]> modelBuilder = CompletableFuture.supplyAsync(() -> buildRows(items));
 515                 viewerUpdater = modelBuilder.thenAcceptAsync(this::setViewerInput, DisplayToolkit.inDisplayThread());
 516                 viewerUpdater.exceptionally(JfrPropertySheet::handleModelBuildException);
 517                 DisplayToolkit.safeTimerExec(Display.getCurrent(), 300, this::showCalculationFeedback);
 518         }
 519 
 520         private void setViewerInput(PropertySheetRow[] rows) {
 521                 if (!viewer.getControl().isDisposed()) {
 522                         viewer.setInput(rows);
 523                 }
 524                 viewerUpdater = null;
 525         }
 526 
 527         private void showCalculationFeedback() {
 528                 if (viewerUpdater != null && !viewer.getControl().isDisposed()) {
 529                         viewer.setInput(new PropertySheetRow[] {CALCULATING});
 530                 }
 531         }
 532 
 533         private static Void handleModelBuildException(Throwable ex) {
 534                 FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Failed to build properties view model", ex); //$NON-NLS-1$
 535                 return null;
 536         }
 537 
 538         private static PropertySheetRow[] buildRows(IItemCollection items) {
 539                 Iterator<? extends IItemIterable> nonEmpty = IteratorToolkit.filter(items.iterator(),
 540                                 i -> i.iterator().hasNext());
 541                 // FIXME: Would it be interesting to add derived attributes here as well?
 542                 Stream<PropertySheetRow> rows = commonAttributes(nonEmpty)
 543                                 .map(attr -> buildProperty(attr, items.iterator(), MAX_DISTINCT_VALUES)).filter(Objects::nonNull);
 544                 return Stream.concat(rows, Stream.of(new PropertySheetRow(null, items))).toArray(PropertySheetRow[]::new);
 545         }
 546 
 547         private static Stream<IAttribute<?>> commonAttributes(Iterator<? extends IItemIterable> iterables)
 548                         throws IllegalArgumentException {
 549                 // FIXME: List of attributes for the item collection should be provided from elsewhere
 550                 if (!iterables.hasNext()) {
 551                         return Stream.empty();
 552                 } else {
 553                         IItemIterable single = iterables.next();
 554                         List<IAttribute<?>> attributes = single.getType().getAttributes();
 555                         if (iterables.hasNext()) {
 556                                 attributes = new ArrayList<>(attributes); // modifiable copy
 557                                 while (iterables.hasNext()) {
 558                                         IType<?> otherType = iterables.next().getType();
 559                                         // FIXME: Use a Set<IType<?>> to avoid going through any type more than once.
 560                                         Iterator<IAttribute<?>> aIterator = attributes.iterator();
 561                                         while (aIterator.hasNext()) {
 562                                                 if (!otherType.hasAttribute(aIterator.next())) {
 563                                                         aIterator.remove();
 564                                                 }
 565                                         }
 566                                 }
 567                         }
 568                         // FIXME: Possible remove this filter if we convert this to persistable attributes.
 569                         return attributes.stream().filter(a -> a.getContentType() != UnitLookup.STACKTRACE);
 570                 }
 571         }
 572 
 573         public static Stream<IFilterFlavor> calculatePersistableFilterFlavors(
 574                 IItemCollection srcItems, IItemCollection dstItems, IItemCollection allItems,
 575                 List<IAttribute<?>> dstAttributes) {
 576                 return calculatePersistableFilterFlavors(srcItems, dstItems, allItems, dstAttributes, a -> true);
 577         }
 578 
 579         public static Stream<IFilterFlavor> calculatePersistableFilterFlavors(
 580                 IItemCollection srcItems, IItemCollection dstItems, IItemCollection allItems, List<IAttribute<?>> dstAttributes,
 581                 Predicate<IAttribute<?>> include) {
 582                 // FIXME: Calculate common content types from the dstItems, and see if any of the srcItems can deliver them?
 583                 Stream<IAttribute<?>> commonAttributes = null;
 584                 if (dstAttributes != null && !dstAttributes.isEmpty()) {
 585                         commonAttributes = commonAttributes(srcItems.iterator()).filter(a -> dstAttributes.contains(a));
 586                 } else {
 587                         Stream<? extends IItemIterable> items = Stream.concat(ItemCollectionToolkit.stream(srcItems),
 588                                         ItemCollectionToolkit.stream(dstItems));
 589                         commonAttributes = commonAttributes(items.iterator());
 590                 }
 591                 Stream<IAttribute<?>> persistableAttributes = DataPageToolkit.getPersistableAttributes(commonAttributes)
 592                                 .filter(include::test);
 593                 // FIXME: Add combinations here as well, similar to PropertySheetRowSelection.buildFlavors
 594                 // FIXME: Can we get construct a life time filter from start and end times?
 595                 return persistableAttributes.map(attr -> buildProperty(attr, srcItems.iterator(), MAX_DISTINCT_VALUES))
 596                                 .filter(p -> p != null && p.value != TOO_MANY_VALUES).sorted(RELEVANCE_ORDER)
 597                                 .map(p -> IPropertyFlavor.build(p.attribute, p.value, allItems));
 598         }
 599 
 600         private static final int MAX_DISTINCT_VALUES = 10;
 601 
 602         // FIXME: How to order? (currently quantity attributes last). Should we involve relational key attributes?
 603         private static final Comparator<PropertySheetRow> RELEVANCE_ORDER = new Comparator<PropertySheetRow>() {
 604 
 605                 @Override
 606                 public int compare(PropertySheetRow o1, PropertySheetRow o2) {
 607                         int a1c = getAttributeCategory(o1.getAttribute());
 608                         int a2c = getAttributeCategory(o2.getAttribute());
 609                         if (a1c == a2c) {
 610                                 return o1.getAttribute().getIdentifier().compareTo(o2.getAttribute().getIdentifier());
 611                         }
 612                         return Integer.compare(a1c, a2c);
 613                 }
 614 
 615                 private int getAttributeCategory(IAttribute<?> attr) {
 616                         ContentType<?> ct = attr.getContentType();
 617                         if (ct.equals(UnitLookup.TIMESTAMP)) {
 618                                 return 0;
 619                         } else if (ct instanceof KindOfQuantity) {
 620                                 return 2;
 621                         }
 622                         return 1;
 623                 }
 624 
 625         };
 626 
 627         private static <M> PropertySheetRow buildProperty(
 628                 IAttribute<M> attribute, Iterator<? extends IItemIterable> iterables, int maxDistinct) {
 629                 ContentType<M> contentType = attribute.getContentType();
 630                 if (contentType instanceof KindOfQuantity) {
 631                         @SuppressWarnings("unchecked")
 632                         IAttribute<IQuantity> qAttribute = (IAttribute<IQuantity>) attribute;
 633                         IQuantity minValue = null;
 634                         IQuantity maxValue = null;
 635                         while (iterables.hasNext()) {
 636                                 IItemIterable ii = iterables.next();
 637                                 IMemberAccessor<IQuantity, IItem> accessor = qAttribute.getAccessor(ii.getType());
 638                                 Iterator<? extends IItem> items = ii.iterator();
 639                                 while (items.hasNext()) {
 640                                         IQuantity val = accessor.getMember(items.next());
 641                                         if (val == null) {
 642                                                 // FIXME: Should null values be expected/accepted?
 643 //                                              FlightRecorderUI.getDefault().getLogger().warning("Null value in " + qAttribute.getIdentifier() + " field"); //$NON-NLS-1$ //$NON-NLS-2$
 644                                         } else if (minValue == null) {
 645                                                 minValue = maxValue = val;
 646                                         } else {
 647                                                 minValue = QuantitiesToolkit.min(val, minValue);
 648                                                 maxValue = QuantitiesToolkit.max(val, maxValue);
 649                                         }
 650                                 }
 651                         }
 652 
 653                         if (minValue != null) {
 654                                 if (minValue == maxValue) {
 655                                         return new PropertySheetRow(qAttribute, minValue);
 656                                 } else {
 657                                         return new PropertySheetRow(qAttribute, QuantityRange.createWithEnd(minValue, maxValue));
 658                                 }
 659                         }
 660                 } else if (contentType instanceof RangeContentType) {
 661                         if (((RangeContentType<?>) contentType).getEndPointContentType() instanceof KindOfQuantity) {
 662                                 @SuppressWarnings("unchecked")
 663                                 IAttribute<IRange<IQuantity>> rangeAttribute = (IAttribute<IRange<IQuantity>>) attribute;
 664                                 IQuantity minValue = null;
 665                                 IQuantity maxValue = null;
 666                                 while (iterables.hasNext()) {
 667                                         IItemIterable ii = iterables.next();
 668                                         IMemberAccessor<IRange<IQuantity>, IItem> accessor = rangeAttribute.getAccessor(ii.getType());
 669                                         Iterator<? extends IItem> items = ii.iterator();
 670                                         while (items.hasNext()) {
 671                                                 IRange<IQuantity> range = accessor.getMember(items.next());
 672                                                 if (range == null) {
 673                                                         // FIXME: Should null values be expected/accepted?
 674 //                                                      FlightRecorderUI.getDefault().getLogger().warning("Null value in " + rangeAttribute.getIdentifier() + " field"); //$NON-NLS-1$ //$NON-NLS-2$
 675                                                 } else if (minValue == null) {
 676                                                         minValue = range.getStart();
 677                                                         maxValue = range.getEnd();
 678                                                 } else {
 679                                                         minValue = QuantitiesToolkit.min(range.getStart(), minValue);
 680                                                         maxValue = QuantitiesToolkit.max(range.getEnd(), maxValue);
 681                                                 }
 682                                         }
 683                                 }
 684 
 685                                 if (minValue != null) {
 686                                         if (minValue == maxValue) {
 687                                                 return new PropertySheetRow(rangeAttribute, minValue);
 688                                         } else {
 689                                                 return new PropertySheetRow(rangeAttribute, QuantityRange.createWithEnd(minValue, maxValue));
 690                                         }
 691                                 }
 692                         }
 693                 }
 694 
 695                 Set<M> keys = new HashSet<>();
 696                 while (iterables.hasNext()) {
 697                         IItemIterable ii = iterables.next();
 698                         IMemberAccessor<M, IItem> accessor = attribute.getAccessor(ii.getType());
 699                         Iterator<? extends IItem> items = ii.iterator();
 700                         while (items.hasNext()) {
 701                                 if (keys.size() > maxDistinct) {
 702                                         return new PropertySheetRow(attribute, TOO_MANY_VALUES);
 703                                 }
 704                                 // FIXME: Add more limitations if there are a lot of items?
 705                                 keys.add(accessor.getMember(items.next()));
 706                         }
 707                 }
 708                 if (keys.size() == 0) {
 709                         return null;
 710                 } else if (keys.size() == 1) {
 711                         return new PropertySheetRow(attribute, keys.iterator().next());
 712                 } else {
 713                         return new PropertySheetRow(attribute, keys);
 714                 }
 715 
 716         }
 717 
 718         @Override
 719         public Control getControl() {
 720                 return viewer.getControl();
 721         }
 722 
 723         @Override
 724         public void setFocus() {
 725                 viewer.getControl().setFocus();
 726         }
 727 
 728 }