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