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         private static void updateSelectedTypes(Object value) {
 343                 if (value instanceof IType<?>) {
 344                         selectedTypes = new HashSet<IType<?>>();
 345                         selectedTypes.add((IType<?>) value);
 346                 }
 347                 else if (value instanceof Collection<?>) {
 348                         selectedTypes = new HashSet<IType<?>>((Collection<IType<?>>) value)
 349                                         .stream().collect(Collectors.toSet());
 350                 }
 351         }
 352 
 353         private static String limitedDeepToString(Object[] array, Function<Object, String> valueToStringProvider) {
 354                 return limitedDeepToString(array, new StringBuilder(), true, valueToStringProvider);
 355         }
 356 
 357         private static String limitedDeepToString(
 358                 Object[] array, StringBuilder builder, boolean isRootArray, Function<Object, String> valueToStringProvider) {
 359                 int maxCharacters = FlightRecorderUI.getDefault().getPreferenceStore()
 360                                 .getInt(PreferenceKeys.PROPERTY_MAXIMUM_PROPERTIES_ARRAY_STRING_SIZE);
 361                 int omitted = 0;
 362                 builder.append('[');
 363                 for (int i = 0; i < array.length; i++) {
 364                         Object element = array[i];
 365                         if (element != null && element.getClass().isArray()) {
 366                                 limitedDeepToString((Object[]) element, builder, false, valueToStringProvider);
 367                         } else {
 368                                 builder.append(valueToStringProvider.apply(element));
 369                         }
 370                         if ((i < (array.length - 1)) && builder.length() < maxCharacters) {
 371                                 builder.append(',');
 372                                 builder.append(' ');
 373                         }
 374                         if (isRootArray && (builder.length() > maxCharacters)) {
 375                                 builder.setLength(maxCharacters);
 376                                 builder.append(Messages.JFR_PROPERTIES_INSERTED_ELLIPSIS);
 377                                 omitted = (array.length - 1) - i;
 378                                 break;
 379                         }
 380                 }
 381                 if (isRootArray && omitted > 0) {
 382                         builder.append(' ');
 383                         if (omitted > 1) {
 384                                 builder.append(MessageFormat.format(Messages.JFR_PROPERTIES_ARRAY_WITH_OMITTED_ELEMENTS, omitted));
 385                         } else {
 386                                 builder.append(Messages.JFR_PROPERTIES_ARRAY_WITH_OMITTED_ELEMENT);
 387                         }
 388                 }
 389                 builder.append(']');
 390                 return builder.toString();
 391         }
 392 
 393         private static final IColumn VERBOSE_VALUE_COLUMN = new ColumnBuilder(Messages.JFR_PROPERTY_SHEET_VERBOSE_VALUE,
 394                         "verboseValue", //$NON-NLS-1$
 395                         new TypedLabelProvider<PropertySheetRow>(PropertySheetRow.class) {
 396                                 @Override
 397                                 protected String getTextTyped(PropertySheetRow p) {
 398                                         Object value = p.getValue();
 399                                         if (p == CALCULATING) {
 400                                                 return Messages.JFR_PROPERTIES_CALCULATING;
 401                                         } else if (value == TOO_MANY_VALUES) {
 402                                                 return Messages.JFR_PROPERTIES_TOO_MANY_VALUES;
 403                                         }
 404                                         return JfrPropertySheet.getVerboseString(value);
 405                                 };
 406 
 407                                 @Override
 408                                 protected String getToolTipTextTyped(PropertySheetRow p) {
 409                                         return getTextTyped(p);
 410                                 };
 411 
 412                         }).build();
 413 
 414         // FIXME: Merge with TypeHandling.getVerboseString
 415         private static String getVerboseString(Object value) {
 416                 if (value instanceof IDisplayable) {
 417                         return ((IDisplayable) value).displayUsing(IDisplayable.VERBOSE);
 418                 } else if (value instanceof IItemCollection) {
 419                         return ItemCollectionToolkit.getDescription(((IItemCollection) value));
 420                 } else if (value instanceof IDescribable) {
 421                         return ((IDescribable) value).getDescription();
 422                 } else if (value instanceof IDescribable[] && ((IDescribable[]) value).length > 0) {
 423                         IDescribable[] values = ((IDescribable[]) value);
 424                         return "[" + values[0].getDescription() + " ... " //$NON-NLS-1$ //$NON-NLS-2$
 425                                         + values[values.length - 1].getDescription() + "]"; //$NON-NLS-1$
 426                 } else if (value instanceof Object[]) {
 427                         return limitedDeepToString((Object[]) value, JfrPropertySheet::getVerboseString);
 428                 } else if (value instanceof Collection) {
 429                         return limitedDeepToString(((Collection<?>) value).toArray(), JfrPropertySheet::getVerboseString);
 430                 }
 431 
 432                 return TypeHandling.getVerboseString(value);
 433         }
 434 
 435         private TableViewer viewer;
 436         private final IPageContainer controller;
 437         private CompletableFuture<Void> viewerUpdater;
 438 
 439         JfrPropertySheet(IPageContainer controller) {
 440                 this.controller = controller;
 441         }
 442 
 443         @Override
 444         public void createControl(Composite parent) {
 445                 viewer = new TableViewer(parent, SWT.MULTI | SWT.FULL_SELECTION);
 446                 viewer.setContentProvider(ArrayContentProvider.getInstance());
 447                 // FIXME: Should we keep a state for the properties view?
 448                 ColumnManager manager = ColumnManager.build(viewer,
 449                                 Arrays.asList(FIELD_COLUMN, VALUE_COLUMN, VERBOSE_VALUE_COLUMN), getTableSettings(null));
 450                 MCContextMenuManager mm = MCContextMenuManager.create(viewer.getControl());
 451                 ColumnMenusFactory.addDefaultMenus(manager, mm);
 452                 Function<Consumer<IFlavoredSelection>, Function<List<PropertySheetRow>, Runnable>> actionProvider = flavorConsumer -> selected -> {
 453                         if (selected.size() == 1 && selected.get(0).value != TOO_MANY_VALUES) {
 454                                 if (selected.get(0).attribute != null) {
 455                                         return () -> flavorConsumer.accept(new PropertySheetRowSelection(selected.get(0)));
 456                                 } else if (selected.get(0).value instanceof IItemCollection) {
 457                                         IItemCollection items = (IItemCollection) selected.get(0).value;
 458                                         String selectionName = itemCollectionDescription(items);
 459                                         return () -> flavorConsumer.accept(new ItemBackedSelection(items, selectionName));
 460                                 }
 461                         }
 462                         return null;
 463                 };
 464                 // FIXME: Break out to other place where these actions are added to menus
 465                 IAction addPageAction = ActionToolkit.action(() -> DataPageToolkit.addPage(selectedTypes),
 466                                 Messages.EventBrowserPage_NEW_PAGE_USING_TYPES_ACTION, NEW_PAGE_ICON);
 467                 mm.appendToGroup(MCContextMenuManager.GROUP_NEW, addPageAction);
 468                 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT,
 469                                 ActionToolkit.forListSelection(viewer, Messages.STORE_SELECTION_ACTION, false,
 470                                                 actionProvider.apply(controller.getSelectionStore()::addSelection)));
 471                 mm.appendToGroup(MCContextMenuManager.GROUP_EDIT,
 472                                 ActionToolkit.forListSelection(viewer, Messages.STORE_AND_ACTIVATE_SELECTION_ACTION, false,
 473                                                 actionProvider.apply(controller.getSelectionStore()::addAndSetAsCurrentSelection)));
 474                 ColumnViewerToolTipSupport.enableFor(viewer);
 475                 PlatformUI.getWorkbench().getHelpSystem().setHelp(viewer.getControl(), HELP_CONTEXT_ID);
 476 
 477                 if (UIPlugin.getDefault().getAccessibilityMode()) {
 478                         FocusTracker.enableFocusTracking(viewer.getTable());
 479                 }
 480         }
 481 
 482         private static TableSettings getTableSettings(IState state) {
 483                 if (state == null) {
 484                         return new TableSettings(null,
 485                                         Arrays.asList(new ColumnSettings(FIELD_COLUMN.getId(), false, 120, null),
 486                                                         new ColumnSettings(VALUE_COLUMN.getId(), false, 120, null),
 487                                                         new ColumnSettings(VERBOSE_VALUE_COLUMN.getId(), true, 400, null)));
 488                 } else {
 489                         return new TableSettings(state);
 490                 }
 491         }
 492 
 493         private static String itemCollectionDescription(IItemCollection items) {
 494                 IQuantity count = items.getAggregate(Aggregators.count());
 495                 return NLS.bind(Messages.JFR_PROPERTY_SHEET_EVENTS, count == null ? 0 : count.displayUsing(IDisplayable.AUTO));
 496         }
 497 
 498         @Override
 499         public void selectionChanged(IWorkbenchPart part, ISelection selection) {
 500                 if (selection instanceof IStructuredSelection) {
 501                         Object first = ((IStructuredSelection) selection).getFirstElement();
 502                         IItemCollection items = AdapterUtil.getAdapter(first, IItemCollection.class);
 503                         if (items != null) {
 504                                 show(items);
 505                         }
 506                 }
 507         }
 508 
 509         private void show(IItemCollection items) {
 510                 if (viewerUpdater != null) {
 511                         viewerUpdater.complete(null);
 512                 }
 513                 CompletableFuture<PropertySheetRow[]> modelBuilder = CompletableFuture.supplyAsync(() -> buildRows(items));
 514                 viewerUpdater = modelBuilder.thenAcceptAsync(this::setViewerInput, DisplayToolkit.inDisplayThread());
 515                 viewerUpdater.exceptionally(JfrPropertySheet::handleModelBuildException);
 516                 DisplayToolkit.safeTimerExec(Display.getCurrent(), 300, this::showCalculationFeedback);
 517         }
 518 
 519         private void setViewerInput(PropertySheetRow[] rows) {
 520                 if (!viewer.getControl().isDisposed()) {
 521                         viewer.setInput(rows);
 522                 }
 523                 viewerUpdater = null;
 524         }
 525 
 526         private void showCalculationFeedback() {
 527                 if (viewerUpdater != null && !viewer.getControl().isDisposed()) {
 528                         viewer.setInput(new PropertySheetRow[] {CALCULATING});
 529                 }
 530         }
 531 
 532         private static Void handleModelBuildException(Throwable ex) {
 533                 FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Failed to build properties view model", ex); //$NON-NLS-1$
 534                 return null;
 535         }
 536 
 537         private static PropertySheetRow[] buildRows(IItemCollection items) {
 538                 Iterator<? extends IItemIterable> nonEmpty = IteratorToolkit.filter(items.iterator(),
 539                                 i -> i.iterator().hasNext());
 540                 // FIXME: Would it be interesting to add derived attributes here as well?
 541                 Stream<PropertySheetRow> rows = commonAttributes(nonEmpty)
 542                                 .map(attr -> buildProperty(attr, items.iterator(), MAX_DISTINCT_VALUES)).filter(Objects::nonNull);
 543                 return Stream.concat(rows, Stream.of(new PropertySheetRow(null, items))).toArray(PropertySheetRow[]::new);
 544         }
 545 
 546         private static Stream<IAttribute<?>> commonAttributes(Iterator<? extends IItemIterable> iterables)
 547                         throws IllegalArgumentException {
 548                 // FIXME: List of attributes for the item collection should be provided from elsewhere
 549                 if (!iterables.hasNext()) {
 550                         return Stream.empty();
 551                 } else {
 552                         IItemIterable single = iterables.next();
 553                         List<IAttribute<?>> attributes = single.getType().getAttributes();
 554                         if (iterables.hasNext()) {
 555                                 attributes = new ArrayList<>(attributes); // modifiable copy
 556                                 while (iterables.hasNext()) {
 557                                         IType<?> otherType = iterables.next().getType();
 558                                         // FIXME: Use a Set<IType<?>> to avoid going through any type more than once.
 559                                         Iterator<IAttribute<?>> aIterator = attributes.iterator();
 560                                         while (aIterator.hasNext()) {
 561                                                 if (!otherType.hasAttribute(aIterator.next())) {
 562                                                         aIterator.remove();
 563                                                 }
 564                                         }
 565                                 }
 566                         }
 567                         // FIXME: Possible remove this filter if we convert this to persistable attributes.
 568                         return attributes.stream().filter(a -> a.getContentType() != UnitLookup.STACKTRACE);
 569                 }
 570         }
 571 
 572         public static Stream<IFilterFlavor> calculatePersistableFilterFlavors(
 573                 IItemCollection srcItems, IItemCollection dstItems, IItemCollection allItems,
 574                 List<IAttribute<?>> dstAttributes) {
 575                 return calculatePersistableFilterFlavors(srcItems, dstItems, allItems, dstAttributes, a -> true);
 576         }
 577 
 578         public static Stream<IFilterFlavor> calculatePersistableFilterFlavors(
 579                 IItemCollection srcItems, IItemCollection dstItems, IItemCollection allItems, List<IAttribute<?>> dstAttributes,
 580                 Predicate<IAttribute<?>> include) {
 581                 // FIXME: Calculate common content types from the dstItems, and see if any of the srcItems can deliver them?
 582                 Stream<IAttribute<?>> commonAttributes = null;
 583                 if (dstAttributes != null && !dstAttributes.isEmpty()) {
 584                         commonAttributes = commonAttributes(srcItems.iterator()).filter(a -> dstAttributes.contains(a));
 585                 } else {
 586                         Stream<? extends IItemIterable> items = Stream.concat(ItemCollectionToolkit.stream(srcItems),
 587                                         ItemCollectionToolkit.stream(dstItems));
 588                         commonAttributes = commonAttributes(items.iterator());
 589                 }
 590                 Stream<IAttribute<?>> persistableAttributes = DataPageToolkit.getPersistableAttributes(commonAttributes)
 591                                 .filter(include::test);
 592                 // FIXME: Add combinations here as well, similar to PropertySheetRowSelection.buildFlavors
 593                 // FIXME: Can we get construct a life time filter from start and end times?
 594                 return persistableAttributes.map(attr -> buildProperty(attr, srcItems.iterator(), MAX_DISTINCT_VALUES))
 595                                 .filter(p -> p != null && p.value != TOO_MANY_VALUES).sorted(RELEVANCE_ORDER)
 596                                 .map(p -> IPropertyFlavor.build(p.attribute, p.value, allItems));
 597         }
 598 
 599         private static final int MAX_DISTINCT_VALUES = 10;
 600 
 601         // FIXME: How to order? (currently quantity attributes last). Should we involve relational key attributes?
 602         private static final Comparator<PropertySheetRow> RELEVANCE_ORDER = new Comparator<PropertySheetRow>() {
 603 
 604                 @Override
 605                 public int compare(PropertySheetRow o1, PropertySheetRow o2) {
 606                         int a1c = getAttributeCategory(o1.getAttribute());
 607                         int a2c = getAttributeCategory(o2.getAttribute());
 608                         if (a1c == a2c) {
 609                                 return o1.getAttribute().getIdentifier().compareTo(o2.getAttribute().getIdentifier());
 610                         }
 611                         return Integer.compare(a1c, a2c);
 612                 }
 613 
 614                 private int getAttributeCategory(IAttribute<?> attr) {
 615                         ContentType<?> ct = attr.getContentType();
 616                         if (ct.equals(UnitLookup.TIMESTAMP)) {
 617                                 return 0;
 618                         } else if (ct instanceof KindOfQuantity) {
 619                                 return 2;
 620                         }
 621                         return 1;
 622                 }
 623 
 624         };
 625 
 626         private static <M> PropertySheetRow buildProperty(
 627                 IAttribute<M> attribute, Iterator<? extends IItemIterable> iterables, int maxDistinct) {
 628                 ContentType<M> contentType = attribute.getContentType();
 629                 if (contentType instanceof KindOfQuantity) {
 630                         @SuppressWarnings("unchecked")
 631                         IAttribute<IQuantity> qAttribute = (IAttribute<IQuantity>) attribute;
 632                         IQuantity minValue = null;
 633                         IQuantity maxValue = null;
 634                         while (iterables.hasNext()) {
 635                                 IItemIterable ii = iterables.next();
 636                                 IMemberAccessor<IQuantity, IItem> accessor = qAttribute.getAccessor(ii.getType());
 637                                 Iterator<? extends IItem> items = ii.iterator();
 638                                 while (items.hasNext()) {
 639                                         IQuantity val = accessor.getMember(items.next());
 640                                         if (val == null) {
 641                                                 // FIXME: Should null values be expected/accepted?
 642 //                                              FlightRecorderUI.getDefault().getLogger().warning("Null value in " + qAttribute.getIdentifier() + " field"); //$NON-NLS-1$ //$NON-NLS-2$
 643                                         } else if (minValue == null) {
 644                                                 minValue = maxValue = val;
 645                                         } else {
 646                                                 minValue = QuantitiesToolkit.min(val, minValue);
 647                                                 maxValue = QuantitiesToolkit.max(val, maxValue);
 648                                         }
 649                                 }
 650                         }
 651 
 652                         if (minValue != null) {
 653                                 if (minValue == maxValue) {
 654                                         return new PropertySheetRow(qAttribute, minValue);
 655                                 } else {
 656                                         return new PropertySheetRow(qAttribute, QuantityRange.createWithEnd(minValue, maxValue));
 657                                 }
 658                         }
 659                 } else if (contentType instanceof RangeContentType) {
 660                         if (((RangeContentType<?>) contentType).getEndPointContentType() instanceof KindOfQuantity) {
 661                                 @SuppressWarnings("unchecked")
 662                                 IAttribute<IRange<IQuantity>> rangeAttribute = (IAttribute<IRange<IQuantity>>) attribute;
 663                                 IQuantity minValue = null;
 664                                 IQuantity maxValue = null;
 665                                 while (iterables.hasNext()) {
 666                                         IItemIterable ii = iterables.next();
 667                                         IMemberAccessor<IRange<IQuantity>, IItem> accessor = rangeAttribute.getAccessor(ii.getType());
 668                                         Iterator<? extends IItem> items = ii.iterator();
 669                                         while (items.hasNext()) {
 670                                                 IRange<IQuantity> range = accessor.getMember(items.next());
 671                                                 if (range == null) {
 672                                                         // FIXME: Should null values be expected/accepted?
 673 //                                                      FlightRecorderUI.getDefault().getLogger().warning("Null value in " + rangeAttribute.getIdentifier() + " field"); //$NON-NLS-1$ //$NON-NLS-2$
 674                                                 } else if (minValue == null) {
 675                                                         minValue = range.getStart();
 676                                                         maxValue = range.getEnd();
 677                                                 } else {
 678                                                         minValue = QuantitiesToolkit.min(range.getStart(), minValue);
 679                                                         maxValue = QuantitiesToolkit.max(range.getEnd(), maxValue);
 680                                                 }
 681                                         }
 682                                 }
 683 
 684                                 if (minValue != null) {
 685                                         if (minValue == maxValue) {
 686                                                 return new PropertySheetRow(rangeAttribute, minValue);
 687                                         } else {
 688                                                 return new PropertySheetRow(rangeAttribute, QuantityRange.createWithEnd(minValue, maxValue));
 689                                         }
 690                                 }
 691                         }
 692                 }
 693 
 694                 Set<M> keys = new HashSet<>();
 695                 while (iterables.hasNext()) {
 696                         IItemIterable ii = iterables.next();
 697                         IMemberAccessor<M, IItem> accessor = attribute.getAccessor(ii.getType());
 698                         Iterator<? extends IItem> items = ii.iterator();
 699                         while (items.hasNext()) {
 700                                 if (keys.size() > maxDistinct) {
 701                                         return new PropertySheetRow(attribute, TOO_MANY_VALUES);
 702                                 }
 703                                 // FIXME: Add more limitations if there are a lot of items?
 704                                 keys.add(accessor.getMember(items.next()));
 705                         }
 706                 }
 707                 if (keys.size() == 0) {
 708                         return null;
 709                 } else if (keys.size() == 1) {
 710                         return new PropertySheetRow(attribute, keys.iterator().next());
 711                 } else {
 712                         return new PropertySheetRow(attribute, keys);
 713                 }
 714 
 715         }
 716 
 717         @Override
 718         public Control getControl() {
 719                 return viewer.getControl();
 720         }
 721 
 722         @Override
 723         public void setFocus() {
 724                 viewer.getControl().setFocus();
 725         }
 726 
 727 }