tmf: Bug 567888: TmfTreeCompositeDataProvider doesn't consider scope

Allows the composite data provider to receive the same entryId from
multiple data providers. If they are within the same scope, a single
entry will be returned to the caller fetching the tree and duplicates
will be discarded.

The order of entries from a single data provider is preserved in most
cases but the order of independent entries from different data providers
is undefined.

[Fixed] Bug 567888: TmfTreeCompositeDataProvider doesn't consider scope
which can lead to ID clashes

Change-Id: I5aa416434623cadfaa295a813de8ef9da67c2e5d
Signed-off-by: Patrick Tasse <patrick.tasse@gmail.com>
Reviewed-on: https://git.eclipse.org/r/c/tracecompass/org.eclipse.tracecompass/+/178911
Tested-by: Trace Compass Bot <tracecompass-bot@eclipse.org>
Tested-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
Reviewed-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
diff --git a/tmf/org.eclipse.tracecompass.tmf.core.tests/src/org/eclipse/tracecompass/tmf/core/tests/model/tree/TmfTreeDataModelTest.java b/tmf/org.eclipse.tracecompass.tmf.core.tests/src/org/eclipse/tracecompass/tmf/core/tests/model/tree/TmfTreeDataModelTest.java
index 1d31e36..f81e379 100644
--- a/tmf/org.eclipse.tracecompass.tmf.core.tests/src/org/eclipse/tracecompass/tmf/core/tests/model/tree/TmfTreeDataModelTest.java
+++ b/tmf/org.eclipse.tracecompass.tmf.core.tests/src/org/eclipse/tracecompass/tmf/core/tests/model/tree/TmfTreeDataModelTest.java
@@ -1,5 +1,5 @@
 /**********************************************************************
- * Copyright (c) 2020 Ericsson
+ * Copyright (c) 2020, 2021 Ericsson
  *
  * All rights reserved. This program and the accompanying materials are
  * made available under the terms of the Eclipse Public License 2.0 which
@@ -59,17 +59,20 @@
     private static final long ID0 = 0L;
     private static final long PARENT_ID0 = -1L;
     private static final OutputElementStyle STYLE0 = null;
+
     private static final @NonNull List<@NonNull String> LABELS1 = Arrays.asList("label4, label5, label6", "label7");
     private static final long ID1 = 1L;
     private static final long PARENT_ID1 = 0L;
     private static final boolean HAS_MODEL1 = false;
     private static final @NonNull OutputElementStyle STYLE1 = new OutputElementStyle("1");
 
-    private static final long ID2 = 0L;
+    private static final long ID2 = 2L;
     private static final long PARENT_ID2 = -1L;
     private static final OutputElementStyle STYLE3 = null;
     private static final String NAME = "Name";
 
+    private static final String SCOPE = "scope";
+
     private TmfTreeDataModel fModel0 = null;
     private TmfTreeDataModel fModel1 = null;
 
@@ -203,7 +206,7 @@
         assertEquals(TO_STRING, "<name=[label1, label2, label3] id=0 parentId=-1 style=null hasRowModel=true>", fModel0.toString());
         assertEquals(TO_STRING, "<name=[label4, label5, label6, label7] id=1 parentId=0 style=Style [1, {}] hasRowModel=false>", fModel1.toString());
         TmfTreeDataModel model2 = createModel(2);
-        assertEquals(TO_STRING, "<name=[Name] id=0 parentId=-1 style=null hasRowModel=true>", model2.toString());
+        assertEquals(TO_STRING, "<name=[Name] id=2 parentId=-1 style=null hasRowModel=true>", model2.toString());
     }
 
     /**
@@ -212,7 +215,7 @@
     @Test
     public void testCompositeTree() {
         List<DummyDataProvider> ddps = new ArrayList<>();
-        for (int i = 0; i < 2; i++) {
+        for (int i = 0; i < 3; i++) {
             ddps.add(new DummyDataProvider(i));
         }
         TmfTreeCompositeDataProvider<@NonNull TmfTreeDataModel, @NonNull DummyDataProvider> composite = new TmfTreeCompositeDataProvider<>(ddps, "composite-dummy");
@@ -222,26 +225,26 @@
         TmfTreeModel<@NonNull TmfTreeDataModel> model = tree.getModel();
         assertNotNull(model);
         assertEquals(Arrays.asList("header"), model.getHeaders());
-        assertEquals(2, model.getEntries().size());
+        assertEquals(3, model.getEntries().size());
         // AnnotationCategories
         TmfModelResponse<@NonNull AnnotationCategoriesModel> returnVal = composite.fetchAnnotationCategories(Collections.emptyMap(), monitor);
         AnnotationCategoriesModel categoryModel = returnVal.getModel();
         assertNotNull(categoryModel);
-        assertEquals(Arrays.asList("0","1","common"), categoryModel.getAnnotationCategories());
+        assertEquals(Arrays.asList("0", "1", "2", "common"), categoryModel.getAnnotationCategories());
         // Annotations
         TmfModelResponse<@NonNull AnnotationModel> annotations = composite.fetchAnnotations(Collections.emptyMap(), monitor);
         AnnotationModel annotationsModel = annotations.getModel();
         assertNotNull(annotationsModel);
         Collection<@NonNull Annotation> collection = annotationsModel.getAnnotations().get("test");
         assertNotNull(collection);
-        assertEquals(4, collection.size());
+        assertEquals(6, collection.size());
     }
 
     // ------------------------------------------------------------------------
     // Helpers
     // ------------------------------------------------------------------------
 
-    private static TmfTreeDataModel createModel(int i) {
+    private static @NonNull TmfTreeDataModel createModel(int i) {
         switch (i) {
         case 0:
             return new TmfTreeDataModel(ID0, PARENT_ID0, LABELS0);
@@ -264,8 +267,16 @@
 
         @Override
         public TmfModelResponse<TmfTreeModel<TmfTreeDataModel>> fetchTree(@NonNull Map<@NonNull String, @NonNull Object> fetchParameters, @Nullable IProgressMonitor monitor) {
-            TmfTreeDataModel createModel = createModel(fModelNo);
-            TmfModelResponse<TmfTreeModel<TmfTreeDataModel>> response = new TmfModelResponse<>(new TmfTreeModel<>(Arrays.asList("header"), Arrays.asList(createModel)), ITmfResponse.Status.COMPLETED, "");
+            List<TmfTreeDataModel> modelList = new ArrayList<>();
+            TmfTreeDataModel model = createModel(fModelNo);
+            modelList.add(model);
+            long parentId = model.getParentId();
+            while (parentId != -1) {
+                TmfTreeDataModel parent = createModel((int) parentId);
+                modelList.add(0, parent);
+                parentId = parent.getParentId();
+            }
+            TmfModelResponse<TmfTreeModel<TmfTreeDataModel>> response = new TmfModelResponse<>(new TmfTreeModel<>(Arrays.asList("header"), modelList, SCOPE), ITmfResponse.Status.COMPLETED, "");
             return response;
         }
 
diff --git a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/model/tree/TmfTreeCompositeDataProvider.java b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/model/tree/TmfTreeCompositeDataProvider.java
index 4886628..12f519a 100644
--- a/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/model/tree/TmfTreeCompositeDataProvider.java
+++ b/tmf/org.eclipse.tracecompass.tmf.core/src/org/eclipse/tracecompass/internal/tmf/core/model/tree/TmfTreeCompositeDataProvider.java
@@ -1,5 +1,5 @@
 /**********************************************************************
- * Copyright (c) 2017 Ericsson
+ * Copyright (c) 2017, 2021 Ericsson
  *
  * All rights reserved. This program and the accompanying materials are
  * made available under the terms of the Eclipse Public License 2.0 which
@@ -11,12 +11,16 @@
 
 package org.eclipse.tracecompass.internal.tmf.core.model.tree;
 
+import java.util.AbstractMap.SimpleEntry;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.jdt.annotation.NonNull;
@@ -34,7 +38,9 @@
 import org.eclipse.tracecompass.tmf.core.response.TmfModelResponse;
 import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Table;
 
 /**
  * Represents a base implementation of {@link ITmfTreeDataProvider} that
@@ -101,15 +107,53 @@
     @Override
     public TmfModelResponse<TmfTreeModel<M>> fetchTree(Map<String, Object> fetchParameters, @Nullable IProgressMonitor monitor) {
         boolean isComplete = true;
-        ImmutableList.Builder<M> series = ImmutableList.builder();
+        List<Entry<M, Object>> entries = new ArrayList<>();
         List<ITableColumnDescriptor> columnDescriptor = null;
 
+        Table<Object, Long, @NonNull M> scopedEntries = HashBasedTable.create();
         for (P dataProvider : fProviders) {
+            Map<Long, AtomicInteger> indexMap = new HashMap<>();
             TmfModelResponse<TmfTreeModel<M>> response = dataProvider.fetchTree(fetchParameters, monitor);
             isComplete &= response.getStatus() == ITmfResponse.Status.COMPLETED;
             TmfTreeModel<M> model = response.getModel();
             if (model != null) {
-                series.addAll(model.getEntries());
+                Object scope = (model.getScope() == null) ? dataProvider : model.getScope();
+                Map<Long, @NonNull M> row = scopedEntries.row(scope);
+                for (M entry : model.getEntries()) {
+                    M previous = row.putIfAbsent(entry.getId(), entry);
+                    // Ignore duplicate entries from different data providers
+                    if (previous == null) {
+                        if (entry.getParentId() == -1) {
+                            entries.add(new SimpleEntry(entry, scope));
+                        } else {
+                            /*
+                             * Insert new entries from subsequent data providers
+                             * at the correct position in the entries list. New
+                             * entries are inserted before sibling entries from
+                             * previous data providers.
+                             */
+                            int index = indexMap.computeIfAbsent(entry.getParentId(), l -> new AtomicInteger()).getAndIncrement();
+                            int pos = 0;
+                            while (pos < entries.size()) {
+                                Entry<M, Object> added = entries.get(pos);
+                                if (added.getValue().equals(scope) && added.getKey().getParentId() == entry.getParentId()) {
+                                    if (index == 0) {
+                                        break;
+                                    }
+                                    index--;
+                                }
+                                pos++;
+                            }
+                            if (pos < entries.size()) {
+                                entries.add(pos, new SimpleEntry(entry, scope));
+                            } else {
+                                entries.add(new SimpleEntry(entry, scope));
+                            }
+                        }
+                    } else {
+                        indexMap.computeIfAbsent(entry.getParentId(), l -> new AtomicInteger()).getAndIncrement();
+                    }
+                }
                 // Use the column descriptor of the first model. All descriptors are supposed to be the same
                 if (columnDescriptor == null) {
                     columnDescriptor = model.getColumnDescriptors();
@@ -125,7 +169,7 @@
             columnDescriptor = Collections.emptyList();
         }
         treeModelBuilder.setColumnDescriptors(columnDescriptor)
-                        .setEntries(series.build());
+                        .setEntries(Lists.transform(entries, e -> e.getKey()));
 
         if (isComplete) {
             return new TmfModelResponse<>(treeModelBuilder.build(), ITmfResponse.Status.COMPLETED, CommonStatusMessage.COMPLETED);