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.pages;
  34 
  35 import java.util.ArrayList;
  36 import java.util.Collections;
  37 import java.util.Iterator;
  38 import java.util.List;
  39 import java.util.Objects;
  40 import java.util.Set;
  41 import java.util.stream.Collectors;
  42 import java.util.stream.Stream;
  43 
  44 import org.eclipse.jface.action.IAction;
  45 import org.eclipse.jface.action.Separator;
  46 import org.eclipse.jface.resource.ImageDescriptor;
  47 import org.eclipse.jface.viewers.ISelection;
  48 import org.eclipse.jface.viewers.TreePath;
  49 import org.eclipse.osgi.util.NLS;
  50 import org.eclipse.swt.SWT;
  51 import org.eclipse.swt.custom.SashForm;
  52 import org.eclipse.swt.layout.FillLayout;
  53 import org.eclipse.swt.widgets.Composite;
  54 import org.eclipse.swt.widgets.Control;
  55 import org.eclipse.ui.forms.widgets.Form;
  56 import org.eclipse.ui.forms.widgets.FormToolkit;
  57 import org.openjdk.jmc.common.IState;
  58 import org.openjdk.jmc.common.IWritableState;
  59 import org.openjdk.jmc.common.item.IAttribute;
  60 import org.openjdk.jmc.common.item.IItem;
  61 import org.openjdk.jmc.common.item.IItemCollection;
  62 import org.openjdk.jmc.common.item.IItemFilter;
  63 import org.openjdk.jmc.common.item.IMemberAccessor;
  64 import org.openjdk.jmc.common.item.IType;
  65 import org.openjdk.jmc.common.item.ItemFilters;
  66 import org.openjdk.jmc.common.item.ItemToolkit;
  67 import org.openjdk.jmc.common.unit.ContentType;
  68 import org.openjdk.jmc.common.unit.IQuantity;
  69 import org.openjdk.jmc.common.unit.IRange;
  70 import org.openjdk.jmc.common.unit.LinearKindOfQuantity;
  71 import org.openjdk.jmc.common.util.StateToolkit;
  72 import org.openjdk.jmc.flightrecorder.JfrAttributes;
  73 import org.openjdk.jmc.flightrecorder.ui.EventTypeFolderNode.EventTypeNode;
  74 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
  75 import org.openjdk.jmc.flightrecorder.ui.IDataPageFactory;
  76 import org.openjdk.jmc.flightrecorder.ui.IDisplayablePage;
  77 import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
  78 import org.openjdk.jmc.flightrecorder.ui.IPageDefinition;
  79 import org.openjdk.jmc.flightrecorder.ui.IPageUI;
  80 import org.openjdk.jmc.flightrecorder.ui.ItemCollectionToolkit;
  81 import org.openjdk.jmc.flightrecorder.ui.RuleManager;
  82 import org.openjdk.jmc.flightrecorder.ui.StreamModel;
  83 import org.openjdk.jmc.flightrecorder.ui.common.AbstractDataPage;
  84 import org.openjdk.jmc.flightrecorder.ui.common.DataPageToolkit;
  85 import org.openjdk.jmc.flightrecorder.ui.common.FlavorSelector;
  86 import org.openjdk.jmc.flightrecorder.ui.common.FlavorSelector.FlavorSelectorState;
  87 import org.openjdk.jmc.flightrecorder.ui.common.ImageConstants;
  88 import org.openjdk.jmc.flightrecorder.ui.common.ItemList;
  89 import org.openjdk.jmc.flightrecorder.ui.common.ItemList.ItemListBuilder;
  90 import org.openjdk.jmc.flightrecorder.ui.common.TypeFilterBuilder;
  91 import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages;
  92 import org.openjdk.jmc.flightrecorder.ui.selection.SelectionStoreActionToolkit;
  93 import org.openjdk.jmc.ui.OrientationAction;
  94 import org.openjdk.jmc.ui.column.ColumnManager.SelectionState;
  95 import org.openjdk.jmc.ui.column.TableSettings;
  96 import org.openjdk.jmc.ui.column.TableSettings.ColumnSettings;
  97 import org.openjdk.jmc.ui.handlers.ActionToolkit;
  98 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
  99 import org.openjdk.jmc.ui.misc.PersistableSashForm;
 100 
 101 public class EventBrowserPage extends AbstractDataPage {
 102         private static final ImageDescriptor NEW_PAGE_ICON = FlightRecorderUI.getDefault()
 103                         .getMCImageDescriptor(ImageConstants.ICON_NEW_PAGE);
 104 
 105         public static class Factory implements IDataPageFactory {
 106 
 107                 @Override
 108                 public String getName(IState state) {
 109                         return Messages.EventBrowserPage_PAGE_NAME;
 110                 }
 111 
 112                 @Override
 113                 public String getDescription(IState state) {
 114                         return Messages.EventBrowserPage_PAGE_DESC;
 115                 }
 116 
 117                 @Override
 118                 public String[] getTopics(IState state) {
 119                         // All topics
 120                         return new String[] {RuleManager.UNMAPPED_REMAINDER_TOPIC};
 121                 }
 122 
 123                 @Override
 124                 public ImageDescriptor getImageDescriptor(IState state) {
 125                         return FlightRecorderUI.getDefault().getMCImageDescriptor(ImageConstants.ICON_EVENT_TYPE_SELECTOR);
 126                 }
 127 
 128                 @Override
 129                 public IDisplayablePage createPage(IPageDefinition dpd, StreamModel items, IPageContainer editor) {
 130                         return new EventBrowserPage(dpd, items, editor);
 131                 }
 132 
 133         }
 134 
 135         @Override
 136         public String getName() {
 137                 return super.getName();
 138         }
 139 
 140         @Override
 141         public IPageUI display(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) {
 142                 return new EventBrowserUI(parent, toolkit, state, editor);
 143         }
 144 
 145         private SelectionState tableSelection;
 146         private ISelection treeSelection;
 147         public TreePath[] treeExpansion;
 148         public FlavorSelectorState flavorSelectorState;
 149 //      public int topIndex;
 150 
 151         public EventBrowserPage(IPageDefinition definition, StreamModel items, IPageContainer editor) {
 152                 super(definition, items, editor);
 153         }
 154 
 155         @Override
 156         public IItemFilter getDefaultSelectionFilter() {
 157                 return ItemFilters.all();
 158         }
 159 
 160         class EventBrowserUI implements IPageUI {
 161 
 162                 private static final String TREE_SASH = "treeSash"; //$NON-NLS-1$
 163                 private static final String ITEM_LIST = "itemList"; //$NON-NLS-1$
 164                 private static final String SHOW_TYPES_WITHOUT_EVENTS = "showTypesWithoutEvents"; //$NON-NLS-1$
 165                 private ItemList list;
 166                 private final SashForm treeSash;
 167                 private final IPageContainer container;
 168                 private final List<ColumnSettings> listColumns = new ArrayList<>();
 169                 private String listOrderBy;
 170                 private Set<IType<?>> selectedTypes = Collections.emptySet();
 171                 private final TypeFilterBuilder typeFilterTree;
 172                 private IItemCollection selectionItems;
 173                 private FlavorSelector flavorSelector;
 174                 private Boolean showTypesWithoutEvents;
 175 
 176                 EventBrowserUI(Composite parent, FormToolkit toolkit, IState state, IPageContainer container) {
 177                         this.container = container;
 178 
 179                         Form form = DataPageToolkit.createForm(parent, toolkit, getName(), getIcon());
 180 
 181                         treeSash = new SashForm(form.getBody(), SWT.HORIZONTAL);
 182                         toolkit.adapt(treeSash);
 183                         typeFilterTree = DataPageToolkit.buildEventTypeTree(treeSash, toolkit, this::onTypeChange, false);
 184                         MCContextMenuManager mm = typeFilterTree.getMenuManager();
 185                         IAction addPageAction = ActionToolkit.action(() -> DataPageToolkit.addPage(selectedTypes),
 186                                         Messages.EventBrowserPage_NEW_PAGE_USING_TYPES_ACTION, NEW_PAGE_ICON);
 187                         mm.appendToGroup(MCContextMenuManager.GROUP_NEW, addPageAction);
 188 
 189                         IAction typesWithoutEventsAction = ActionToolkit.checkAction(this::setTypesWithoutEvents,
 190                                         Messages.EventBrowserPage_DISPLAY_TYPES_WITHOUT_EVENTS, null);
 191                         showTypesWithoutEvents = StateToolkit.readBoolean(state, SHOW_TYPES_WITHOUT_EVENTS, true);
 192                         typesWithoutEventsAction.setChecked(showTypesWithoutEvents);
 193                         mm.appendToGroup(MCContextMenuManager.GROUP_OPEN, typesWithoutEventsAction);
 194 
 195                         SelectionStoreActionToolkit.addSelectionStoreActions(typeFilterTree.getViewer(), () -> selectedTypes,
 196                                         container.getSelectionStore(), Messages.EventBrowserPage_EVENT_TYPE_TREE_SELECTION, mm);
 197 
 198                         Composite listParent = toolkit.createComposite(treeSash);
 199                         listParent.setLayout(new FillLayout());
 200                         PersistableSashForm.loadState(treeSash, state.getChild(TREE_SASH));
 201 
 202                         form.getToolBarManager().add(addPageAction);
 203                         form.getToolBarManager().add(new Separator());
 204                         OrientationAction.installActions(form, treeSash);
 205 
 206                         IState itemListState = state.getChild(ITEM_LIST);
 207                         if (itemListState != null) {
 208                                 TableSettings settings = new TableSettings(itemListState);
 209                                 listColumns.addAll(settings.getColumns());
 210                                 listOrderBy = settings.getOrderBy();
 211                         }
 212                         list = new ItemListBuilder().build(listParent, null);
 213 
 214                         flavorSelector = FlavorSelector.itemsWithTimerange(form, null, getDataSource().getItems(), container,
 215                                         this::onInputSelected, flavorSelectorState);
 216 
 217                         addResultActions(form);
 218                         if (treeExpansion != null) {
 219                                 typeFilterTree.getViewer().setExpandedTreePaths(treeExpansion);
 220                         } else {
 221                                 typeFilterTree.getViewer().expandAll();
 222                         }
 223                         typeFilterTree.getViewer().setSelection(treeSelection);
 224 //                      if (topIndex >= 0) {
 225 //                              typeFilterTree.getViewer().getTree().setTopItem(typeFilterTree.getViewer().getTree().getItem(topIndex));
 226 //                      }
 227                         list.getManager().setSelectionState(tableSelection);
 228                 }
 229 
 230                 private void setTypesWithoutEvents(boolean checked) {
 231                         showTypesWithoutEvents = checked;
 232                         refreshTree();
 233                 }
 234 
 235                 private void onInputSelected(IItemCollection items, IRange<IQuantity> timeRange) {
 236                         this.selectionItems = (items == null) ? getDataSource().getItems() : items;
 237                         refreshTree();
 238                 }
 239 
 240                 private void refreshTree() {
 241                         boolean noTypesWereSelected = selectedTypes.isEmpty();
 242 
 243                         typeFilterTree.getViewer().getControl().setRedraw(false);
 244                         TreePath[] expansion = typeFilterTree.getViewer().getExpandedTreePaths();
 245                         ISelection selection = typeFilterTree.getViewer().getSelection();
 246                         typeFilterTree.setInput(getDataSource().getTypeTree((ItemCollectionToolkit.stream(selectionItems)
 247                                         .filter(ii -> showTypesWithoutEvents || ii.hasItems()))));
 248                         typeFilterTree.getViewer().setExpandedTreePaths(expansion);
 249                         typeFilterTree.getViewer().setSelection(selection);
 250                         typeFilterTree.getViewer().getControl().setRedraw(true);
 251                         typeFilterTree.getViewer().getControl().redraw();
 252 
 253                         if (noTypesWereSelected) {
 254                                 // force re-interpretation of empty type selection
 255                                 rebuildItemList();
 256                         }
 257                 }
 258 
 259                 private IItemCollection getFilteredItems() {
 260                         if (!selectedTypes.isEmpty()) {
 261                                 Set<String> types = selectedTypes.stream().map(t -> t.getIdentifier()).collect(Collectors.toSet());
 262                                 return selectionItems.apply(ItemFilters.type(types));
 263                         }
 264                         return selectionItems;
 265                 }
 266 
 267                 private void onTypeChange() {
 268                         Set<IType<?>> oldSelectedTypes = selectedTypes;
 269                         selectedTypes = typeFilterTree.getSelectedTypes().map(EventTypeNode::getType).collect(Collectors.toSet());
 270                         if (!Objects.equals(selectedTypes, oldSelectedTypes)) {
 271                                 container.showSelection(getFilteredItems());
 272                                 rebuildItemList();
 273                         }
 274                 }
 275 
 276                 private void rebuildItemList() {
 277                         mergeListSettings();
 278 
 279                         Iterator<? extends IType<?>> types = selectedTypes.iterator();
 280                         IItemCollection filteredItems = getFilteredItems();
 281                         if (selectedTypes.isEmpty()) {
 282                                 types = ItemCollectionToolkit.stream(selectionItems).map(is -> is.getType()).distinct().iterator();
 283                         }
 284 
 285                         // FIXME: Possibly move to attribute toolkit/handler?
 286                         // FIXME: Make sure to get Event Type as the first column
 287                         // FIXME: Stream<IType> -> Stream<IAttribute> should be delegated to some context (e.g. the editor)
 288                         Stream<IAttribute<?>> commonAttributes = Stream.empty();
 289                         if (types.hasNext()) {
 290                                 List<IAttribute<?>> attributes = types.next().getAttributes();
 291                                 if (types.hasNext()) {
 292                                         while (types.hasNext()) {
 293                                                 attributes = types.next().getAttributes().stream().filter(attributes::contains)
 294                                                                 .collect(Collectors.toList());
 295                                         }
 296                                         commonAttributes = attributes.stream();
 297                                 } else {
 298                                         commonAttributes = attributes.stream().filter(a -> !a.equals(JfrAttributes.EVENT_TYPE));
 299                                 }
 300                                 commonAttributes = commonAttributes.filter(a -> !a.equals(JfrAttributes.EVENT_STACKTRACE));
 301                         }
 302 
 303                         String orderBy = listOrderBy;
 304                         Set<String> existingColumnIds = listColumns.stream().map(ColumnSettings::getId).collect(Collectors.toSet());
 305                         List<ColumnSettings> newColumns = new ArrayList<>();
 306                         ItemListBuilder itemListBuilder = new ItemListBuilder();
 307                         commonAttributes.forEach(a -> {
 308                                 String combinedId = ItemList.getColumnId(a);
 309                                 ContentType<?> contentType = a.getContentType();
 310                                 IMemberAccessor<?, IItem> accessor = ItemToolkit.accessor(a);
 311                                 // FIXME: This is duplicated in JfrPropertySheet, where we also create a tooltip for an attribute.
 312                                 itemListBuilder.addColumn(combinedId, a.getName(),
 313                                                 NLS.bind(Messages.ATTRIBUTE_ID_LABEL, a.getIdentifier()) + System.getProperty("line.separator") //$NON-NLS-1$
 314                                                                 + NLS.bind(Messages.ATTRIBUTE_DESCRIPTION_LABEL, a.getDescription()),
 315                                                 contentType instanceof LinearKindOfQuantity, accessor);
 316                                 if (combinedId.equals(listOrderBy)) {
 317                                         // the list now has the most current order, to allow the list to clear it
 318                                         listOrderBy = null;
 319                                 }
 320                                 if (!existingColumnIds.contains(combinedId)) {
 321                                         newColumns.add(0, new ColumnSettings(combinedId, false, null, null));
 322                                 }
 323                         });
 324                         listColumns.addAll(0, newColumns);
 325 
 326                         Control oldListControl = list.getManager().getViewer().getControl();
 327                         Composite parent = oldListControl.getParent();
 328                         oldListControl.dispose();
 329                         list = DataPageToolkit.createSimpleItemList(parent, itemListBuilder, container,
 330                                         DataPageToolkit.createTableSettingsByOrderByAndColumnsWithDefaultOrdering(orderBy, listColumns),
 331                                         Messages.EventBrowserPage_EVENT_BROWSER_SELECTION);
 332                         parent.layout();
 333                         list.show(filteredItems);
 334                 }
 335 
 336                 private void mergeListSettings() {
 337                         TableSettings settings = list.getManager().getSettings();
 338                         Set<String> columns = settings.getColumns().stream().map(ColumnSettings::getId).collect(Collectors.toSet());
 339                         List<Integer> replaceIndexs = new ArrayList<>(columns.size());
 340                         for (int i = 0; i < listColumns.size(); i++) {
 341                                 if (columns.contains(listColumns.get(i).getId())) {
 342                                         replaceIndexs.add(i);
 343                                 }
 344                         }
 345                         Iterator<ColumnSettings> replacements = settings.getColumns().iterator();
 346                         Iterator<Integer> indexs = replaceIndexs.iterator();
 347                         while (indexs.hasNext() && replacements.hasNext()) {
 348                                 listColumns.set(indexs.next(), replacements.next());
 349                         }
 350                         if (settings.getOrderBy() != null) {
 351                                 listOrderBy = settings.getOrderBy();
 352                         }
 353                 }
 354 
 355                 @Override
 356                 public void saveTo(IWritableState state) {
 357                         PersistableSashForm.saveState(treeSash, state.createChild(TREE_SASH));
 358                         mergeListSettings();
 359                         new TableSettings(listOrderBy, listColumns).saveState(state.createChild(ITEM_LIST));
 360                         StateToolkit.writeBoolean(state, SHOW_TYPES_WITHOUT_EVENTS, showTypesWithoutEvents);
 361                         saveToLocal();
 362                 }
 363 
 364                 private void saveToLocal() {
 365                         treeSelection = typeFilterTree.getViewer().getSelection();
 366                         treeExpansion = typeFilterTree.getViewer().getExpandedTreePaths();
 367                         // FIXME: indexOf doesn't seem to work for some reason, probably an SWT bug
 368 //                      topIndex = typeFilterTree.getViewer().getTree().indexOf(typeFilterTree.getViewer().getTree().getTopItem());
 369                         tableSelection = list.getManager().getSelectionState();
 370                         flavorSelectorState = flavorSelector.getFlavorSelectorState();
 371                 }
 372         }
 373 }