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