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