/* * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. * * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The contents of this file are subject to the terms of either the Universal Permissive License * v 1.0 as shown at http://oss.oracle.com/licenses/upl * * or the following license: * * Redistribution and use in source and binary forms, with or without modification, are permitted * provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of conditions * and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other materials provided with * the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors may be used to * endorse or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.openjdk.jmc.flightrecorder.ui; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jface.action.IAction; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.Page; import org.eclipse.ui.views.properties.IPropertySheetPage; import org.openjdk.jmc.common.IDescribable; import org.openjdk.jmc.common.IDisplayable; import org.openjdk.jmc.common.IState; import org.openjdk.jmc.common.collection.IteratorToolkit; import org.openjdk.jmc.common.item.Aggregators; import org.openjdk.jmc.common.item.IAttribute; import org.openjdk.jmc.common.item.IItem; import org.openjdk.jmc.common.item.IItemCollection; import org.openjdk.jmc.common.item.IItemFilter; import org.openjdk.jmc.common.item.IItemIterable; import org.openjdk.jmc.common.item.IMemberAccessor; import org.openjdk.jmc.common.item.IType; import org.openjdk.jmc.common.unit.ContentType; import org.openjdk.jmc.common.unit.IQuantity; import org.openjdk.jmc.common.unit.IRange; import org.openjdk.jmc.common.unit.KindOfQuantity; import org.openjdk.jmc.common.unit.QuantitiesToolkit; import org.openjdk.jmc.common.unit.QuantityRange; import org.openjdk.jmc.common.unit.RangeContentType; import org.openjdk.jmc.common.unit.UnitLookup; import org.openjdk.jmc.common.util.TypeHandling; import org.openjdk.jmc.flightrecorder.JfrAttributes; import org.openjdk.jmc.flightrecorder.ui.common.DataPageToolkit; import org.openjdk.jmc.flightrecorder.ui.common.ImageConstants; import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages; import org.openjdk.jmc.flightrecorder.ui.preferences.PreferenceKeys; import org.openjdk.jmc.flightrecorder.ui.selection.FlavoredSelectionBase; import org.openjdk.jmc.flightrecorder.ui.selection.IFilterFlavor; import org.openjdk.jmc.flightrecorder.ui.selection.IFlavoredSelection; import org.openjdk.jmc.flightrecorder.ui.selection.IItemStreamFlavor; import org.openjdk.jmc.flightrecorder.ui.selection.IPropertyFlavor; import org.openjdk.jmc.flightrecorder.ui.selection.ItemBackedSelection; import org.openjdk.jmc.ui.TypeAppearance; import org.openjdk.jmc.ui.UIPlugin; import org.openjdk.jmc.ui.accessibility.FocusTracker; import org.openjdk.jmc.ui.column.ColumnBuilder; import org.openjdk.jmc.ui.column.ColumnManager; import org.openjdk.jmc.ui.column.ColumnMenusFactory; import org.openjdk.jmc.ui.column.IColumn; import org.openjdk.jmc.ui.column.TableSettings; import org.openjdk.jmc.ui.column.TableSettings.ColumnSettings; import org.openjdk.jmc.ui.common.util.AdapterUtil; import org.openjdk.jmc.ui.handlers.ActionToolkit; import org.openjdk.jmc.ui.handlers.MCContextMenuManager; import org.openjdk.jmc.ui.misc.DisplayToolkit; import org.openjdk.jmc.ui.misc.TypedLabelProvider; // FIXME: fields - units - filters - icons etc. should be handled more properly public class JfrPropertySheet extends Page implements IPropertySheetPage { private static final ImageDescriptor NEW_PAGE_ICON = FlightRecorderUI.getDefault() .getMCImageDescriptor(ImageConstants.ICON_NEW_PAGE); private static final String HELP_CONTEXT_ID = FlightRecorderUI.PLUGIN_ID + ".JfrPropertiesView"; //$NON-NLS-1$ private static final Object TOO_MANY_VALUES = new Object(); private static final PropertySheetRow CALCULATING = new PropertySheetRow(null, null); private static Set> selectedTypes = Collections.emptySet(); private static class PropertySheetRowSelection extends FlavoredSelectionBase { private final PropertySheetRow row; PropertySheetRowSelection(PropertySheetRow row) { super(MessageFormat.format(Messages.JFR_PROPERTIES_PROPERTY_SELECTION, row.attribute.getName())); this.row = row; } @Override public Stream getFlavors( IItemFilter dstFilter, IItemCollection items, List> dstAttributes) { /* * FIXME: Is this the desired behavior? Discuss and change if necessary. * * This most likely need more thought and discussion, but the implemented order of * flavors is currently: * * For chart selections: * * 1: The selected events if any of them appear on the destination page * * 2: All events on the destination page in the selected range (if a range was selected) * * 3-n: All events on the destination page filtered on any of the attributes common to * all selected events (excluding the range attribute if a range was selected) * * For histogram and list selections: * * 1: The selected events if any of them appear on the destination page * * 2-n: All events on the destination page filtered on any of the attributes common to * all selected events (all will at least have (endTime)) * * For properties view selections: * * 1: All events on the destination page filtered on the selected attribute:value if * they all have the selected attribute * * 2: All events on the destination page filtered on the selected value if they all have * an attribute with the same content type * * 3: All events on the destination page filtered on all common attributes with values * from all events filtered on the selected attribute:value (see example) * * 4: All events on the destination page filtered on the selected attribute:value if * there are any events with the attribute (and the attribute is not common to all * events in which case this flavor has already been added in (1)) * * Example of properties view selections (3): * * ECID:1-2-3-4 was selected and the user navigates to Java Application. All events on * Java Application share (thread) and (endTime), so all events on the page are filtered * on those properties. The values to include are collected from all events with the * ECID attribute having value 1-2-3-4. The threads will be put in a set, the timestamps * will form a range. */ IItemCollection filteredDstItems = ItemCollectionToolkit.filterIfNotNull(items, dstFilter); IPropertyFlavor relatedFilterFlavor = IPropertyFlavor.build(row.attribute, row.value, filteredDstItems); LinkedList flavors = new LinkedList<>(); boolean anyRelatedOnDst = relatedFilterFlavor.evaluate().hasItems(); IPropertyFlavor selectedPropertyFlavor = IPropertyFlavor.build(row.attribute, row.value, items); if (anyRelatedOnDst) { // prio1(a): Items related to the selected attribute if there are any flavors.add(selectedPropertyFlavor); selectedPropertyFlavor = null; } IItemCollection itemsRelatedToSelection = items.apply(relatedFilterFlavor.getFilter()); if (dstAttributes == null || dstAttributes.isEmpty()) { dstAttributes = commonAttributes(filteredDstItems.iterator()).collect(Collectors.toList()); } Iterator> commonDstAttr = dstAttributes.iterator(); List relatedProperties = new ArrayList<>(); while (commonDstAttr.hasNext()) { IAttribute dstAttribute = commonDstAttr.next(); if (!dstAttribute.equals(JfrAttributes.EVENT_TYPE) && (!(dstAttribute.getContentType() instanceof KindOfQuantity) || dstAttribute.equals(JfrAttributes.END_TIME))) { // FIXME: Collect type or quantity values? if (dstAttribute.equals(row.attribute)) { if (!anyRelatedOnDst && selectedPropertyFlavor != null) { // prio1(b): Related to the selected attribute even though it's empty, since the attribute is shared by all flavors.push(selectedPropertyFlavor); selectedPropertyFlavor = null; } relatedProperties = null; } else if (!dstAttribute.equals(row.attribute) && dstAttribute.getContentType().equals(row.attribute.getContentType())) { // prio2: Destination items with an attribute of the selected content type and which equals the selected value flavors.add(IPropertyFlavor.build(dstAttribute, row.value, items)); } if (relatedProperties != null) { // Collect values from items related to selection (only items of types that has the attribute), and add as filter PropertySheetRow av = buildProperty(dstAttribute, ItemCollectionToolkit.stream(itemsRelatedToSelection) .filter(is -> dstAttribute.getAccessor(is.getType()) != null).iterator(), Integer.MAX_VALUE); if (av != null) { relatedProperties.add(IPropertyFlavor.build(av.attribute, av.value, items)); } } } } if (relatedProperties != null) { if (relatedProperties.size() > 1) { // prio3: Destination items with properties shared with the items related to the selection flavors.add(IPropertyFlavor.combine(relatedProperties::stream, items)); } // FIXME: Combinations with for example two properties if there are three properties in total shared? // prio4: Destination items with one property shared with the items related to the selection flavors.addAll(relatedProperties); } if (selectedPropertyFlavor != null) { // prio4: Items related to the selected attribute even if there aren't any flavors.add(selectedPropertyFlavor); } return flavors.stream(); } } static class PropertySheetRow { final IAttribute attribute; final Object value; PropertySheetRow(IAttribute attribute, Object value) { this.attribute = attribute; this.value = value; } public IAttribute getAttribute() { return attribute; } public Object getValue() { return value; } } private static final IColumn FIELD_COLUMN = new ColumnBuilder(Messages.JFR_PROPERTY_SHEET_FIELD, "field", //$NON-NLS-1$ new TypedLabelProvider(PropertySheetRow.class) { @Override protected String getTextTyped(PropertySheetRow p) { return p.attribute == null ? "" : p.attribute.getName(); //$NON-NLS-1$ }; @Override protected String getToolTipTextTyped(PropertySheetRow p) { // FIXME: This is duplicated in EventBrowserPage, where we also create a tooltip for an attribute. return p.attribute == null ? "" //$NON-NLS-1$ : NLS.bind(Messages.ATTRIBUTE_ID_LABEL, p.attribute.getIdentifier()) + System.getProperty("line.separator") //$NON-NLS-1$ + NLS.bind(Messages.ATTRIBUTE_DESCRIPTION_LABEL, p.attribute.getDescription()); }; @Override protected Image getImageTyped(PropertySheetRow p) { if (p.attribute != null) { Image icon = TypeAppearance.getImage(p.attribute.getContentType().getIdentifier()); return icon == null ? UIPlugin.getDefault().getImage(UIPlugin.ICON_PROPERTY_OBJECT) : icon; } return null; }; }).build(); private static final IColumn VALUE_COLUMN = new ColumnBuilder(Messages.JFR_PROPERTY_SHEET_VALUE, "value", //$NON-NLS-1$ new TypedLabelProvider(PropertySheetRow.class) { @Override protected String getTextTyped(PropertySheetRow p) { Object value = p.getValue(); if (p == CALCULATING) { return Messages.JFR_PROPERTIES_CALCULATING; } else if (value == TOO_MANY_VALUES) { return Messages.JFR_PROPERTIES_TOO_MANY_VALUES; } updateSelectedTypes(value); return getValueString(value); }; // FIXME: Merge with TypeHandling.getValueString @SuppressWarnings("unchecked") private String getValueString(Object value) { if (value instanceof IItemCollection) { return itemCollectionDescription((IItemCollection) value); } else if (value instanceof IDescribable) { return ((IDescribable) value).getName(); } else if (value instanceof IDescribable[] && ((IDescribable[]) value).length > 0) { IDescribable[] values = ((IDescribable[]) value); return "[" + values[0].getName() + " ... " //$NON-NLS-1$ //$NON-NLS-2$ + values[values.length - 1].getName() + "]"; //$NON-NLS-1$ } else if (value instanceof Object[]) { return limitedDeepToString((Object[]) value, this::getValueString); } else if (value instanceof Collection) { System.out.println(value); selectedTypes = new HashSet>((Collection>) value) .stream().collect(Collectors.toSet()); return limitedDeepToString(((Collection) value).toArray(), this::getValueString); } return TypeHandling.getValueString(value); } @Override protected String getToolTipTextTyped(PropertySheetRow p) { Object value = p.getValue(); if (value instanceof IQuantity) { return TypeHandling.getNumericString(((IQuantity) value).numberValue()); } return JfrPropertySheet.getVerboseString(value); }; }).build(); private static void updateSelectedTypes(Object value) { if (value instanceof IType) { selectedTypes = new HashSet>(); selectedTypes.add((IType) value); } else if (value instanceof Collection) { selectedTypes = new HashSet>((Collection>) value) .stream().collect(Collectors.toSet()); } } private static String limitedDeepToString(Object[] array, Function valueToStringProvider) { return limitedDeepToString(array, new StringBuilder(), true, valueToStringProvider); } private static String limitedDeepToString( Object[] array, StringBuilder builder, boolean isRootArray, Function valueToStringProvider) { int maxCharacters = FlightRecorderUI.getDefault().getPreferenceStore() .getInt(PreferenceKeys.PROPERTY_MAXIMUM_PROPERTIES_ARRAY_STRING_SIZE); int omitted = 0; builder.append('['); for (int i = 0; i < array.length; i++) { Object element = array[i]; if (element != null && element.getClass().isArray()) { limitedDeepToString((Object[]) element, builder, false, valueToStringProvider); } else { builder.append(valueToStringProvider.apply(element)); } if ((i < (array.length - 1)) && builder.length() < maxCharacters) { builder.append(','); builder.append(' '); } if (isRootArray && (builder.length() > maxCharacters)) { builder.setLength(maxCharacters); builder.append(Messages.JFR_PROPERTIES_INSERTED_ELLIPSIS); omitted = (array.length - 1) - i; break; } } if (isRootArray && omitted > 0) { builder.append(' '); if (omitted > 1) { builder.append(MessageFormat.format(Messages.JFR_PROPERTIES_ARRAY_WITH_OMITTED_ELEMENTS, omitted)); } else { builder.append(Messages.JFR_PROPERTIES_ARRAY_WITH_OMITTED_ELEMENT); } } builder.append(']'); return builder.toString(); } private static final IColumn VERBOSE_VALUE_COLUMN = new ColumnBuilder(Messages.JFR_PROPERTY_SHEET_VERBOSE_VALUE, "verboseValue", //$NON-NLS-1$ new TypedLabelProvider(PropertySheetRow.class) { @Override protected String getTextTyped(PropertySheetRow p) { Object value = p.getValue(); if (p == CALCULATING) { return Messages.JFR_PROPERTIES_CALCULATING; } else if (value == TOO_MANY_VALUES) { return Messages.JFR_PROPERTIES_TOO_MANY_VALUES; } return JfrPropertySheet.getVerboseString(value); }; @Override protected String getToolTipTextTyped(PropertySheetRow p) { return getTextTyped(p); }; }).build(); // FIXME: Merge with TypeHandling.getVerboseString private static String getVerboseString(Object value) { if (value instanceof IDisplayable) { return ((IDisplayable) value).displayUsing(IDisplayable.VERBOSE); } else if (value instanceof IItemCollection) { return ItemCollectionToolkit.getDescription(((IItemCollection) value)); } else if (value instanceof IDescribable) { return ((IDescribable) value).getDescription(); } else if (value instanceof IDescribable[] && ((IDescribable[]) value).length > 0) { IDescribable[] values = ((IDescribable[]) value); return "[" + values[0].getDescription() + " ... " //$NON-NLS-1$ //$NON-NLS-2$ + values[values.length - 1].getDescription() + "]"; //$NON-NLS-1$ } else if (value instanceof Object[]) { return limitedDeepToString((Object[]) value, JfrPropertySheet::getVerboseString); } else if (value instanceof Collection) { return limitedDeepToString(((Collection) value).toArray(), JfrPropertySheet::getVerboseString); } return TypeHandling.getVerboseString(value); } private TableViewer viewer; private final IPageContainer controller; private CompletableFuture viewerUpdater; JfrPropertySheet(IPageContainer controller) { this.controller = controller; } @Override public void createControl(Composite parent) { viewer = new TableViewer(parent, SWT.MULTI | SWT.FULL_SELECTION); viewer.setContentProvider(ArrayContentProvider.getInstance()); // FIXME: Should we keep a state for the properties view? ColumnManager manager = ColumnManager.build(viewer, Arrays.asList(FIELD_COLUMN, VALUE_COLUMN, VERBOSE_VALUE_COLUMN), getTableSettings(null)); MCContextMenuManager mm = MCContextMenuManager.create(viewer.getControl()); ColumnMenusFactory.addDefaultMenus(manager, mm); Function, Function, Runnable>> actionProvider = flavorConsumer -> selected -> { if (selected.size() == 1 && selected.get(0).value != TOO_MANY_VALUES) { if (selected.get(0).attribute != null) { return () -> flavorConsumer.accept(new PropertySheetRowSelection(selected.get(0))); } else if (selected.get(0).value instanceof IItemCollection) { IItemCollection items = (IItemCollection) selected.get(0).value; String selectionName = itemCollectionDescription(items); return () -> flavorConsumer.accept(new ItemBackedSelection(items, selectionName)); } } return null; }; // FIXME: Break out to other place where these actions are added to menus IAction addPageAction = ActionToolkit.action(() -> DataPageToolkit.addPage(selectedTypes), Messages.EventBrowserPage_NEW_PAGE_USING_TYPES_ACTION, NEW_PAGE_ICON); mm.appendToGroup(MCContextMenuManager.GROUP_NEW, addPageAction); mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, ActionToolkit.forListSelection(viewer, Messages.STORE_SELECTION_ACTION, false, actionProvider.apply(controller.getSelectionStore()::addSelection))); mm.appendToGroup(MCContextMenuManager.GROUP_EDIT, ActionToolkit.forListSelection(viewer, Messages.STORE_AND_ACTIVATE_SELECTION_ACTION, false, actionProvider.apply(controller.getSelectionStore()::addAndSetAsCurrentSelection))); ColumnViewerToolTipSupport.enableFor(viewer); PlatformUI.getWorkbench().getHelpSystem().setHelp(viewer.getControl(), HELP_CONTEXT_ID); if (UIPlugin.getDefault().getAccessibilityMode()) { FocusTracker.enableFocusTracking(viewer.getTable()); } } private static TableSettings getTableSettings(IState state) { if (state == null) { return new TableSettings(null, Arrays.asList(new ColumnSettings(FIELD_COLUMN.getId(), false, 120, null), new ColumnSettings(VALUE_COLUMN.getId(), false, 120, null), new ColumnSettings(VERBOSE_VALUE_COLUMN.getId(), true, 400, null))); } else { return new TableSettings(state); } } private static String itemCollectionDescription(IItemCollection items) { IQuantity count = items.getAggregate(Aggregators.count()); return NLS.bind(Messages.JFR_PROPERTY_SHEET_EVENTS, count == null ? 0 : count.displayUsing(IDisplayable.AUTO)); } @Override public void selectionChanged(IWorkbenchPart part, ISelection selection) { if (selection instanceof IStructuredSelection) { Object first = ((IStructuredSelection) selection).getFirstElement(); IItemCollection items = AdapterUtil.getAdapter(first, IItemCollection.class); if (items != null) { show(items); } } } private void show(IItemCollection items) { if (viewerUpdater != null) { viewerUpdater.complete(null); } CompletableFuture modelBuilder = CompletableFuture.supplyAsync(() -> buildRows(items)); viewerUpdater = modelBuilder.thenAcceptAsync(this::setViewerInput, DisplayToolkit.inDisplayThread()); viewerUpdater.exceptionally(JfrPropertySheet::handleModelBuildException); DisplayToolkit.safeTimerExec(Display.getCurrent(), 300, this::showCalculationFeedback); } private void setViewerInput(PropertySheetRow[] rows) { if (!viewer.getControl().isDisposed()) { viewer.setInput(rows); } viewerUpdater = null; } private void showCalculationFeedback() { if (viewerUpdater != null && !viewer.getControl().isDisposed()) { viewer.setInput(new PropertySheetRow[] {CALCULATING}); } } private static Void handleModelBuildException(Throwable ex) { FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Failed to build properties view model", ex); //$NON-NLS-1$ return null; } private static PropertySheetRow[] buildRows(IItemCollection items) { Iterator nonEmpty = IteratorToolkit.filter(items.iterator(), i -> i.iterator().hasNext()); // FIXME: Would it be interesting to add derived attributes here as well? Stream rows = commonAttributes(nonEmpty) .map(attr -> buildProperty(attr, items.iterator(), MAX_DISTINCT_VALUES)).filter(Objects::nonNull); return Stream.concat(rows, Stream.of(new PropertySheetRow(null, items))).toArray(PropertySheetRow[]::new); } private static Stream> commonAttributes(Iterator iterables) throws IllegalArgumentException { // FIXME: List of attributes for the item collection should be provided from elsewhere if (!iterables.hasNext()) { return Stream.empty(); } else { IItemIterable single = iterables.next(); List> attributes = single.getType().getAttributes(); if (iterables.hasNext()) { attributes = new ArrayList<>(attributes); // modifiable copy while (iterables.hasNext()) { IType otherType = iterables.next().getType(); // FIXME: Use a Set> to avoid going through any type more than once. Iterator> aIterator = attributes.iterator(); while (aIterator.hasNext()) { if (!otherType.hasAttribute(aIterator.next())) { aIterator.remove(); } } } } // FIXME: Possible remove this filter if we convert this to persistable attributes. return attributes.stream().filter(a -> a.getContentType() != UnitLookup.STACKTRACE); } } public static Stream calculatePersistableFilterFlavors( IItemCollection srcItems, IItemCollection dstItems, IItemCollection allItems, List> dstAttributes) { return calculatePersistableFilterFlavors(srcItems, dstItems, allItems, dstAttributes, a -> true); } public static Stream calculatePersistableFilterFlavors( IItemCollection srcItems, IItemCollection dstItems, IItemCollection allItems, List> dstAttributes, Predicate> include) { // FIXME: Calculate common content types from the dstItems, and see if any of the srcItems can deliver them? Stream> commonAttributes = null; if (dstAttributes != null && !dstAttributes.isEmpty()) { commonAttributes = commonAttributes(srcItems.iterator()).filter(a -> dstAttributes.contains(a)); } else { Stream items = Stream.concat(ItemCollectionToolkit.stream(srcItems), ItemCollectionToolkit.stream(dstItems)); commonAttributes = commonAttributes(items.iterator()); } Stream> persistableAttributes = DataPageToolkit.getPersistableAttributes(commonAttributes) .filter(include::test); // FIXME: Add combinations here as well, similar to PropertySheetRowSelection.buildFlavors // FIXME: Can we get construct a life time filter from start and end times? return persistableAttributes.map(attr -> buildProperty(attr, srcItems.iterator(), MAX_DISTINCT_VALUES)) .filter(p -> p != null && p.value != TOO_MANY_VALUES).sorted(RELEVANCE_ORDER) .map(p -> IPropertyFlavor.build(p.attribute, p.value, allItems)); } private static final int MAX_DISTINCT_VALUES = 10; // FIXME: How to order? (currently quantity attributes last). Should we involve relational key attributes? private static final Comparator RELEVANCE_ORDER = new Comparator() { @Override public int compare(PropertySheetRow o1, PropertySheetRow o2) { int a1c = getAttributeCategory(o1.getAttribute()); int a2c = getAttributeCategory(o2.getAttribute()); if (a1c == a2c) { return o1.getAttribute().getIdentifier().compareTo(o2.getAttribute().getIdentifier()); } return Integer.compare(a1c, a2c); } private int getAttributeCategory(IAttribute attr) { ContentType ct = attr.getContentType(); if (ct.equals(UnitLookup.TIMESTAMP)) { return 0; } else if (ct instanceof KindOfQuantity) { return 2; } return 1; } }; private static PropertySheetRow buildProperty( IAttribute attribute, Iterator iterables, int maxDistinct) { ContentType contentType = attribute.getContentType(); if (contentType instanceof KindOfQuantity) { @SuppressWarnings("unchecked") IAttribute qAttribute = (IAttribute) attribute; IQuantity minValue = null; IQuantity maxValue = null; while (iterables.hasNext()) { IItemIterable ii = iterables.next(); IMemberAccessor accessor = qAttribute.getAccessor(ii.getType()); Iterator items = ii.iterator(); while (items.hasNext()) { IQuantity val = accessor.getMember(items.next()); if (val == null) { // FIXME: Should null values be expected/accepted? // FlightRecorderUI.getDefault().getLogger().warning("Null value in " + qAttribute.getIdentifier() + " field"); //$NON-NLS-1$ //$NON-NLS-2$ } else if (minValue == null) { minValue = maxValue = val; } else { minValue = QuantitiesToolkit.min(val, minValue); maxValue = QuantitiesToolkit.max(val, maxValue); } } } if (minValue != null) { if (minValue == maxValue) { return new PropertySheetRow(qAttribute, minValue); } else { return new PropertySheetRow(qAttribute, QuantityRange.createWithEnd(minValue, maxValue)); } } } else if (contentType instanceof RangeContentType) { if (((RangeContentType) contentType).getEndPointContentType() instanceof KindOfQuantity) { @SuppressWarnings("unchecked") IAttribute> rangeAttribute = (IAttribute>) attribute; IQuantity minValue = null; IQuantity maxValue = null; while (iterables.hasNext()) { IItemIterable ii = iterables.next(); IMemberAccessor, IItem> accessor = rangeAttribute.getAccessor(ii.getType()); Iterator items = ii.iterator(); while (items.hasNext()) { IRange range = accessor.getMember(items.next()); if (range == null) { // FIXME: Should null values be expected/accepted? // FlightRecorderUI.getDefault().getLogger().warning("Null value in " + rangeAttribute.getIdentifier() + " field"); //$NON-NLS-1$ //$NON-NLS-2$ } else if (minValue == null) { minValue = range.getStart(); maxValue = range.getEnd(); } else { minValue = QuantitiesToolkit.min(range.getStart(), minValue); maxValue = QuantitiesToolkit.max(range.getEnd(), maxValue); } } } if (minValue != null) { if (minValue == maxValue) { return new PropertySheetRow(rangeAttribute, minValue); } else { return new PropertySheetRow(rangeAttribute, QuantityRange.createWithEnd(minValue, maxValue)); } } } } Set keys = new HashSet<>(); while (iterables.hasNext()) { IItemIterable ii = iterables.next(); IMemberAccessor accessor = attribute.getAccessor(ii.getType()); Iterator items = ii.iterator(); while (items.hasNext()) { if (keys.size() > maxDistinct) { return new PropertySheetRow(attribute, TOO_MANY_VALUES); } // FIXME: Add more limitations if there are a lot of items? keys.add(accessor.getMember(items.next())); } } if (keys.size() == 0) { return null; } else if (keys.size() == 1) { return new PropertySheetRow(attribute, keys.iterator().next()); } else { return new PropertySheetRow(attribute, keys); } } @Override public Control getControl() { return viewer.getControl(); } @Override public void setFocus() { viewer.getControl().setFocus(); } }