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