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 }