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