blob: 8dba1cf10c755991fe099f0b15838cac94b2e778 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.tracecompass.tmf.ui.viewers.tree;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.FlowScopeLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.FlowScopeLogBuilder;
import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.tmf.core.dataprovider.DataProviderManager;
import org.eclipse.tracecompass.tmf.core.model.filters.TimeQueryFilter;
import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataModel;
import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataProvider;
import org.eclipse.tracecompass.tmf.core.model.tree.TmfTreeDataModel;
import org.eclipse.tracecompass.tmf.core.model.tree.TmfTreeModel;
import org.eclipse.tracecompass.tmf.core.response.ITmfResponse;
import org.eclipse.tracecompass.tmf.core.response.TmfModelResponse;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceOpenedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceSelectedSignal;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceContext;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.eclipse.tracecompass.tmf.ui.viewers.ILegendImageProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.dialogs.MultiTreePatternFilter;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.dialogs.TriStateFilteredCheckboxTree;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Longs;
/**
* Abstract viewer for trees with checkboxes.
*
* @since 3.2
* @author Loic Prieur-Drevon
*/
public abstract class AbstractSelectTreeViewer extends AbstractTmfTreeViewer {
private static final @NonNull Logger LOGGER = TraceCompassLog.getLogger(AbstractSelectTreeViewer.class);
/** Timeout between updates in the updateData thread **/
private static final long BUILD_UPDATE_TIMEOUT = 500;
/** ID of the checked tree items in the map of data in {@link TmfTraceContext} */
private static final @NonNull String CHECKED_ELEMENTS = ".CHECKED_ELEMENTS"; //$NON-NLS-1$
private static final @NonNull String FILTER_STRING = ".FILTER_STRING"; //$NON-NLS-1$
private static final @NonNull String UPDATE_CONTENT_JOB_NAME = "AbstractSelectTreeViewer#updateContent Job"; //$NON-NLS-1$
private static final String FAILED_TO_SLEEP_PREFIX = "Failed to sleep the "; //$NON-NLS-1$
private static final String LOG_CATEGORY_SUFFIX = " Tree viewer"; //$NON-NLS-1$
private final String fId;
private final @NonNull String fLogCategory;
private static final ViewerComparator COMPARATOR = new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
TmfTreeViewerEntry entry1 = (TmfTreeViewerEntry) e1;
TmfTreeViewerEntry entry2 = (TmfTreeViewerEntry) e2;
String name1 = entry1.getName();
String name2 = entry2.getName();
Long longValue1 = Longs.tryParse(name1);
Long longValue2 = Longs.tryParse(name2);
return (longValue1 == null || longValue2 == null) ? name1.compareTo(name2) : longValue1.compareTo(longValue2);
}
};
private final class CheckStateChangedListener implements ICheckStateListener {
@Override
public void checkStateChanged(CheckStateChangedEvent event) {
saveViewContext();
if (fChartViewer != null) {
fChartViewer.handleCheckStateChangedEvent(getCheckedViewerEntries());
// Legend image might have changed
refresh();
}
}
}
private ILegendImageProvider fLegendImageProvider;
private ICheckboxTreeViewerListener fChartViewer;
private TriStateFilteredCheckboxTree fCheckboxTree;
private final int fLegendIndex;
/**
* Constructor
*
* @param parent
* Parent composite
* @param checkboxTree
* <code>TriStateFilteredTree</code> wrapping a
* <code>CheckboxTreeViewer</code>
* @param legendColumnIndex
* index of the legend column (-1 if none)
*/
private AbstractSelectTreeViewer(Composite parent, TriStateFilteredCheckboxTree checkboxTree,
int legendIndex, String id) {
super(parent, checkboxTree.getViewer());
TreeViewer treeViewer = checkboxTree.getViewer();
treeViewer.setComparator(COMPARATOR);
if (treeViewer instanceof CheckboxTreeViewer) {
((CheckboxTreeViewer) treeViewer).addCheckStateListener(new CheckStateChangedListener());
}
checkboxTree.getFilterControl().addModifyListener(e -> saveViewContext());
fCheckboxTree = checkboxTree;
fLegendIndex = legendIndex;
fId = id;
fLogCategory = fId + LOG_CATEGORY_SUFFIX;
}
/**
* Constructor
*
* @param parent
* Parent composite
* @param legendIndex
* index of the legend column (-1 if none)
* @param id
* {@link ITmfTreeDataProvider} ID
*/
public AbstractSelectTreeViewer(Composite parent, int legendIndex, String id) {
// Create the tree viewer with a filtered checkbox
this(parent, new TriStateFilteredCheckboxTree(parent,
SWT.MULTI | SWT.H_SCROLL | SWT.FULL_SELECTION,
new MultiTreePatternFilter(), true), legendIndex, id);
}
/**
* Tell the chart viewer to listen to changes in the tree viewer
*
* @param listener
* Chart listening to changes in the tree's selected entries
*/
public void setTreeListener(ICheckboxTreeViewerListener listener) {
fChartViewer = listener;
}
/**
* Set the legend image provider (provider tree cells with an image).
*
* @param legendImageProvider
* Provides an image legend associated with a name
*/
public void setLegendImageProvider(ILegendImageProvider legendImageProvider) {
fLegendImageProvider = legendImageProvider;
}
/**
* Return the checked state of an element
*
* @param element
* the element
* @return if the element is checked
*/
public boolean isChecked(Object element) {
return fCheckboxTree.getChecked(element);
}
/**
* Select previously checked entries when going back to trace.
*/
@Override
protected void contentChanged(ITmfTreeViewerEntry rootEntry) {
ITmfTrace trace = getTrace();
if (trace == null) {
return;
}
TmfTraceContext ctx = TmfTraceManager.getInstance().getTraceContext(trace);
Set<Long> ids = (Set<Long>) ctx.getData(getClass() + CHECKED_ELEMENTS);
if (ids != null && rootEntry != null) {
List<ITmfTreeViewerEntry> checkedElements = new ArrayList<>();
checkEntries(ids, rootEntry, checkedElements);
fCheckboxTree.setCheckedElements(checkedElements.toArray());
}
Object filterString = ctx.getData(getClass() + FILTER_STRING);
if (filterString instanceof String) {
fCheckboxTree.setFilterText((String) filterString);
} else {
fCheckboxTree.setFilterText(""); //$NON-NLS-1$
}
if (fChartViewer != null) {
fChartViewer.handleCheckStateChangedEvent(getCheckedViewerEntries());
}
getTreeViewer().refresh();
}
private Collection<ITmfTreeViewerEntry> getCheckedViewerEntries() {
Object[] checkedElements = fCheckboxTree.getCheckedElements();
return Lists.newArrayList(Iterables.filter(Arrays.asList(checkedElements), ITmfTreeViewerEntry.class));
}
/**
* Recursively find entries which were previously checked and check them again
* by id.
*
* @param ids
* Set of previously checked IDs
* @param root
* {@link ITmfTreeViewerEntry} to compare to the set of checked
* entries
* @param checkedElements
* list of checked entries to which we add the root entry if it was
* previously checked
*/
private void checkEntries(Set<Long> ids, ITmfTreeViewerEntry root, List<ITmfTreeViewerEntry> checkedElements) {
if (root instanceof TmfGenericTreeEntry
&& ids.contains(((TmfGenericTreeEntry) root).getModel().getId())) {
checkedElements.add(root);
}
for (ITmfTreeViewerEntry child : root.getChildren()) {
checkEntries(ids, child, checkedElements);
}
}
@Override
@TmfSignalHandler
public void traceOpened(TmfTraceOpenedSignal signal) {
saveViewContext();
restorePatternFilter(signal.getTrace());
fCheckboxTree.setCheckedElements(new Object[0]);
super.traceOpened(signal);
}
@Override
@TmfSignalHandler
public void traceSelected(TmfTraceSelectedSignal signal) {
if (getTrace() != signal.getTrace()) {
saveViewContext();
restorePatternFilter(signal.getTrace());
fCheckboxTree.setCheckedElements(new Object[0]);
}
super.traceSelected(signal);
}
@Override
public void reset() {
fCheckboxTree.setCheckedElements(new Object[0]);
fCheckboxTree.setFilterText(""); //$NON-NLS-1$
super.reset();
}
@Override
protected void updateContent(long start, long end, boolean isSelection) {
try (FlowScopeLog scope = new FlowScopeLogBuilder(LOGGER, Level.FINE, UPDATE_CONTENT_JOB_NAME)
.setCategory(fLogCategory).build()) {
ITmfTrace trace = getTrace();
if (trace == null) {
return;
}
Job thread = new Job(UPDATE_CONTENT_JOB_NAME) {
@Override
public IStatus run(IProgressMonitor monitor) {
try (FlowScopeLog runScope = new FlowScopeLogBuilder(LOGGER, Level.FINE, UPDATE_CONTENT_JOB_NAME + " run") //$NON-NLS-1$
.setParentScope(scope).build()) {
ITmfTreeDataProvider<@NonNull ITmfTreeDataModel> provider = getProvider(trace);
if (provider == null) {
return Status.OK_STATUS;
}
Map<String, Object> parameters = getParameters(start, end, isSelection);
if (parameters.isEmpty()) {
return Status.OK_STATUS;
}
boolean isComplete = false;
do {
TmfModelResponse<@NonNull TmfTreeModel<@NonNull ITmfTreeDataModel>> response;
try (FlowScopeLog iterScope = new FlowScopeLogBuilder(LOGGER, Level.FINE, UPDATE_CONTENT_JOB_NAME + " query") //$NON-NLS-1$
.setParentScope(scope).build()) {
response = provider.fetchTree(parameters, monitor);
TmfTreeModel<@NonNull ITmfTreeDataModel> model = response.getModel();
if (model != null) {
updateTree(trace, start, end, model.getEntries());
}
}
ITmfResponse.Status status = response.getStatus();
if (status == ITmfResponse.Status.COMPLETED) {
/* Model is complete, no need to request again the data provider */
isComplete = true;
} else if (status == ITmfResponse.Status.FAILED || status == ITmfResponse.Status.CANCELLED) {
/* Error occurred, return */
isComplete = true;
} else {
/**
* Status is RUNNING. Sleeping current thread to wait before request data
* provider again
**/
try {
Thread.sleep(BUILD_UPDATE_TIMEOUT);
} catch (InterruptedException e) {
/**
* InterruptedException is throw by Thread.Sleep and we should retry querying
* the data provider
**/
runScope.addData(FAILED_TO_SLEEP_PREFIX + getName(), e);
Thread.currentThread().interrupt();
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, FAILED_TO_SLEEP_PREFIX + getName());
}
}
} while (!isComplete);
return Status.OK_STATUS;
}
}
};
thread.setSystem(true);
thread.schedule();
}
}
private void updateTree(ITmfTrace trace, long start, long end, List<@NonNull ITmfTreeDataModel> model) {
try (FlowScopeLog parentScope = new FlowScopeLogBuilder(LOGGER, Level.FINE, "AbstractSelectTreeViewer:TreeUpdateRequested" ) //$NON-NLS-1$
.setCategory(fLogCategory).build()) {
final ITmfTreeViewerEntry rootEntry = modelToTree(start, end, model);
/* Set the input in main thread only if it didn't change */
if (rootEntry != null) {
Display.getDefault().asyncExec(() -> {
try (FlowScopeLog scope = new FlowScopeLogBuilder(LOGGER, Level.FINE, "AbstractSelectTreeViewer:TreeUpdate").setParentScope(parentScope).build()) { //$NON-NLS-1$
if (!trace.equals(getTrace())) {
return;
}
TreeViewer treeViewer = getTreeViewer();
if (treeViewer.getControl().isDisposed()) {
return;
}
Object input = treeViewer.getInput();
if (!(input instanceof ITmfTreeViewerEntry) || !treeEquals(rootEntry, (ITmfTreeViewerEntry) input)) {
treeViewer.setInput(rootEntry);
contentChanged(rootEntry);
} else {
treeViewer.refresh();
}
// FIXME should add a bit of padding
for (TreeColumn column : treeViewer.getTree().getColumns()) {
column.pack();
}
}
});
}
}
}
/**
* Recursively compare two trees, as the tree viewer requires a fast method,
* {@link Object#hashCode()} for example.
*
* @param entry1
* one of the root tree entries to compare
* @param entry2
* the other tree entry to compare
* @return if the trees are equal
*/
private boolean treeEquals(ITmfTreeViewerEntry entry1, ITmfTreeViewerEntry entry2) {
if (!Objects.equals(entry1.getName(), entry2.getName())) {
return false;
}
List<@NonNull ? extends ITmfTreeViewerEntry> children1 = entry1.getChildren();
List<@NonNull ? extends ITmfTreeViewerEntry> children2 = entry2.getChildren();
if (children1.size() != children2.size()) {
return false;
}
int size = children1.size();
for (int i = 0; i < size; i++) {
if (!treeEquals(children1.get(i), children2.get(i))) {
return false;
}
}
return true;
}
/**
* Save the checked entries' ID in the view context before changing trace and
* check them again in the new tree.
*/
private void saveViewContext() {
ITmfTrace previousTrace = getTrace();
if (previousTrace != null) {
Object[] checkedElements = fCheckboxTree.getCheckedElements();
Set<Long> ids = new HashSet<>();
for (Object checkedElement : checkedElements) {
if (checkedElement instanceof TmfGenericTreeEntry) {
ids.add(((TmfGenericTreeEntry) checkedElement).getModel().getId());
}
}
Text filterControl = fCheckboxTree.getFilterControl();
String filterString = filterControl != null ? filterControl.getText() : null;
TmfTraceManager.getInstance().updateTraceContext(previousTrace,
builder -> builder.setData(getClass() + CHECKED_ELEMENTS, ids)
.setData(getClass() + FILTER_STRING, filterString));
}
}
private void restorePatternFilter(ITmfTrace trace) {
if (trace == null) {
fCheckboxTree.getPatternFilter().setPattern(null);
return;
}
TmfTraceContext ctx = TmfTraceManager.getInstance().getTraceContext(trace);
Object filterString = ctx.getData(getClass() + FILTER_STRING);
if (filterString instanceof String) {
fCheckboxTree.getPatternFilter().setPattern((String) filterString);
} else {
fCheckboxTree.getPatternFilter().setPattern(null);
}
}
/**
* Get the legend image for a entry's name
*
* @param name
* the entry's name (used in both Tree and Chart viewer
* @return the correctly dimensioned image if there is a legend image provider
*/
protected Image getLegendImage(@NonNull String name) {
/* If the image height match the row height, row height will increment */
ILegendImageProvider legendImageProvider = fLegendImageProvider;
int legendColumnIndex = fLegendIndex;
if (legendImageProvider != null && legendColumnIndex >= 0) {
Tree tree = getTreeViewer().getTree();
int imageWidth = tree.getColumn(legendColumnIndex).getWidth();
int imageHeight = tree.getItemHeight() - 1;
if (imageHeight > 0 && imageWidth > 0) {
return legendImageProvider.getLegendImage(imageHeight, imageWidth, name);
}
}
return null;
}
@Override
protected ITmfTreeViewerEntry updateElements(ITmfTrace trace, long start, long end, boolean isSelection) {
throw new UnsupportedOperationException("This method should not be called for AbstractSelectTreeViewers"); //$NON-NLS-1$
}
/**
* Getter for the {@link ITmfTreeDataProvider} to query for this TreeViewer
*
* @param trace the trace
* @return the relevant provider, if any
* @since 4.0
*/
protected ITmfTreeDataProvider<@NonNull ITmfTreeDataModel> getProvider(@NonNull ITmfTrace trace) {
return DataProviderManager.getInstance().getDataProvider(trace, fId, ITmfTreeDataProvider.class);
}
@Override
protected void initializeDataSource(@NonNull ITmfTrace trace) {
getProvider(trace);
}
/**
* Get the filter to query the {@link ITmfTreeDataProvider} for the queried parameters
*
* @param start
* query start time
* @param end
* query end time
* @param isSelection
* if the query is a selection
* @return the resulting query filter
* @since 4.0
* @deprecated Use {@link #getParameters(long, long, boolean)} instead
*/
@Deprecated
protected @Nullable TimeQueryFilter getFilter(long start, long end, boolean isSelection) {
return new TimeQueryFilter(Long.min(start, end), Long.max(start, end), 2);
}
/**
* Get the map to query the {@link ITmfTreeDataProvider} for the queried
* parameters
*
* @param start
* query start time
* @param end
* query end time
* @param isSelection
* if the query is a selection
* @return the resulting query parameters
* @since 5.0
*/
protected @NonNull Map<String, Object> getParameters(long start, long end, boolean isSelection) {
return FetchParametersUtils.timeQueryToMap(new TimeQueryFilter(Long.min(start, end), Long.max(start, end), 2));
}
/**
* Algorithm to convert a model (List of {@link TmfTreeDataModel}) to the tree.
*
* @param start
* queried start time
* @param end
* queried end time
* @param model
* model to convert
* @return the resulting {@link TmfTreeViewerEntry}.
*/
protected ITmfTreeViewerEntry modelToTree(long start, long end, List<ITmfTreeDataModel> model) {
TmfTreeViewerEntry root = new TmfTreeViewerEntry(StringUtils.EMPTY);
Map<Long, TmfTreeViewerEntry> map = new HashMap<>();
map.put(-1L, root);
for (ITmfTreeDataModel entry : model) {
TmfGenericTreeEntry<ITmfTreeDataModel> viewerEntry = new TmfGenericTreeEntry<>(entry);
map.put(entry.getId(), viewerEntry);
TmfTreeViewerEntry parent = map.get(entry.getParentId());
if (parent != null && !parent.getChildren().contains(viewerEntry)) {
parent.addChild(viewerEntry);
}
}
return root;
}
/**
* Create a sortable column
*
* @param text
* column label
* @param comparator
* comparator to sort {@link TmfGenericTreeEntry}s
* @return the comparator
*/
protected static <T extends TmfGenericTreeEntry<? extends TmfTreeDataModel>>
@NonNull TmfTreeColumnData createColumn(String text, Comparator<T> comparator) {
TmfTreeColumnData column = new TmfTreeColumnData(text);
column.setComparator(new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
return comparator.compare((T) e1, (T) e2);
}
});
return column;
}
/**
* Get the full path of the entry, from the trace to itself, to query the
* presentation provider.
*
* @param entry
* entry whose legend needs to be resolved.
* @return the relevant series name.
*/
protected static @NonNull String getFullPath(TmfGenericTreeEntry<TmfTreeDataModel> entry) {
StringBuilder path = new StringBuilder(entry.getName());
ITmfTreeViewerEntry parent = entry.getParent();
while (parent instanceof TmfGenericTreeEntry) {
path.insert(0, parent.getName() + '/');
parent = parent.getParent();
}
return path.toString();
}
/**
* Get the checkbox tree in this viewer.
*
* @return the checkbox tree.
* @since 4.0
*/
public TriStateFilteredCheckboxTree getTriStateFilteredCheckboxTree() {
return fCheckboxTree;
}
}