]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19558 Refactor events
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Fri, 16 Jun 2023 18:17:03 +0000 (13:17 -0500)
committersonartech <sonartech@sonarsource.com>
Tue, 20 Jun 2023 20:02:59 +0000 (20:02 +0000)
16 files changed:
server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistEventsStepIT.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/event/EventRepository.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/event/EventRepositoryImpl.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistEventsStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/QualityGateEventsStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStep.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/event/EventRepositoryImplTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/QualityGateEventsStepTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/QualityProfileEventsStepTest.java
server/sonar-db-dao/src/it/java/org/sonar/db/component/SnapshotDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/SnapshotDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/SnapshotMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/SnapshotMapper.xml
server/sonar-webserver-webapi/src/it/java/org/sonar/server/developers/ws/SearchEventsActionNewIssuesIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/developers/ws/SearchEventsActionQualityGateIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/developers/ws/SearchEventsAction.java

index cdc42a8f8edbf78b81938035e6051485e2dc5a18..1fda6e1f29454acf8779a3ccdaea37b2e2ae4197 100644 (file)
@@ -42,7 +42,6 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.event.EventDto;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
@@ -94,7 +93,7 @@ public class PersistEventsStepIT extends BaseStepTest {
   public void setup() {
     analysisMetadataHolder.setAnalysisDate(someDate.getTime()).setUuid(ANALYSIS_UUID);
     underTest = new PersistEventsStep(dbTester.getDbClient(), system2, treeRootHolder, analysisMetadataHolder, eventRepository, uuidFactory);
-    when(eventRepository.getEvents(any(Component.class))).thenReturn(Collections.emptyList());
+    when(eventRepository.getEvents()).thenReturn(Collections.emptyList());
   }
 
   @Override
@@ -143,7 +142,7 @@ public class PersistEventsStepIT extends BaseStepTest {
     when(system2.now()).thenReturn(NOW);
     treeRootHolder.setRoot(ROOT);
     Event alert = Event.createAlert("Failed", null, "Open issues > 0");
-    when(eventRepository.getEvents(ROOT)).thenReturn(ImmutableList.of(alert));
+    when(eventRepository.getEvents()).thenReturn(ImmutableList.of(alert));
 
     underTest.execute(new TestComputationStepContext());
 
@@ -167,7 +166,7 @@ public class PersistEventsStepIT extends BaseStepTest {
     when(system2.now()).thenReturn(NOW);
     treeRootHolder.setRoot(ROOT);
     Event profile = Event.createProfile("foo", null, "bar");
-    when(eventRepository.getEvents(ROOT)).thenReturn(ImmutableList.of(profile));
+    when(eventRepository.getEvents()).thenReturn(ImmutableList.of(profile));
 
     underTest.execute(new TestComputationStepContext());
 
index d094ddd226dc784b76d3161c37e1de5559b6f3d1..0aa3d54da7dbb08607424cbc3ffb0285fbd4aaf4 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.event;
 
-import org.sonar.ce.task.projectanalysis.component.Component;
-
 public interface EventRepository {
   /**
-   * @throws NullPointerException if {@code component} or {@code event} is {@code null}
-   * @throws IllegalArgumentException if type of {@code component} is not {@link Component.Type#PROJECT}
+   * @throws NullPointerException if {@code event} is {@code null}
    */
-  void add(Component component, Event event);
+  void add(Event event);
 
-  /**
-   * @throws NullPointerException if {@code component} is {@code null}
-   */
-  Iterable<Event> getEvents(Component component);
+  Iterable<Event> getEvents();
 }
index 35739a3508e39b4f14c6cfa505ddbd21cec19c4c..11df0cce4f6776854ab7d36760b64dbec53d6889 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.event;
 
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Multimap;
-import org.sonar.ce.task.projectanalysis.component.Component;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
 public class EventRepositoryImpl implements EventRepository {
-  private final Multimap<String, Event> events = HashMultimap.create();
+  private final List<Event> events = new LinkedList<>();
 
   @Override
-  public void add(Component component, Event event) {
-    checkArgument(component.getType() == Component.Type.PROJECT, "Component must be of type PROJECT");
-    events.put(component.getUuid(), requireNonNull(event));
+  public void add(Event event) {
+    events.add(requireNonNull(event));
   }
 
   @Override
-  public Iterable<Event> getEvents(Component component) {
-    return this.events.get(component.getUuid());
+  public Iterable<Event> getEvents() {
+    return Collections.unmodifiableList(this.events);
   }
 }
index 51677f0fb4f6f85e75faf76ad5982318fa39c1fd..5535efdbc1a06e18cda899bae0e1253e63bb211e 100644 (file)
@@ -25,10 +25,7 @@ import java.util.stream.StreamSupport;
 import org.sonar.api.utils.System2;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
-import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
-import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
 import org.sonar.ce.task.projectanalysis.event.Event;
 import org.sonar.ce.task.projectanalysis.event.EventRepository;
 import org.sonar.ce.task.step.ComputationStep;
@@ -60,8 +57,7 @@ public class PersistEventsStep implements ComputationStep {
   public void execute(ComputationStep.Context context) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       long analysisDate = analysisMetadataHolder.getAnalysisDate();
-      new DepthTraversalTypeAwareCrawler(new PersistEventComponentVisitor(dbSession, analysisDate))
-        .visit(treeRootHolder.getRoot());
+      new PersistEvent(dbSession, analysisDate).process(treeRootHolder.getRoot());
       dbSession.commit();
     }
   }
@@ -71,18 +67,16 @@ public class PersistEventsStep implements ComputationStep {
     return "Persist events";
   }
 
-  private class PersistEventComponentVisitor extends TypeAwareVisitorAdapter {
+  private class PersistEvent {
     private final DbSession session;
     private final long analysisDate;
 
-    PersistEventComponentVisitor(DbSession session, long analysisDate) {
-      super(CrawlerDepthLimit.PROJECT, Order.PRE_ORDER);
+    PersistEvent(DbSession session, long analysisDate) {
       this.session = session;
       this.analysisDate = analysisDate;
     }
 
-    @Override
-    public void visitProject(Component project) {
+    public void process(Component project) {
       processEvents(session, project, analysisDate);
       saveVersionEvent(session, project, analysisDate);
     }
@@ -94,7 +88,7 @@ public class PersistEventsStep implements ComputationStep {
         .setDescription(event.getDescription())
         .setData(event.getData());
       // FIXME bulk insert
-      for (EventDto batchEventDto : StreamSupport.stream(eventRepository.getEvents(component).spliterator(), false).map(eventToEventDto).toList()) {
+      for (EventDto batchEventDto : StreamSupport.stream(eventRepository.getEvents().spliterator(), false).map(eventToEventDto).toList()) {
         dbClient.eventDao().insert(session, batchEventDto);
       }
     }
index d2e8546c7ca7c527ddc5abfe16883f1920a8b611..2565594c3462e37e5237e6cdea23dd896f056c60 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.step;
 
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.sonar.api.measures.CoreMetrics;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.ce.task.projectanalysis.analysis.Branch;
 import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.ComponentVisitor;
-import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
-import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
-import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
 import org.sonar.ce.task.projectanalysis.event.Event;
 import org.sonar.ce.task.projectanalysis.event.EventRepository;
 import org.sonar.ce.task.projectanalysis.measure.Measure;
@@ -43,10 +43,6 @@ import org.sonar.server.notification.NotificationService;
 import org.sonar.server.qualitygate.notification.QGChangeNotification;
 
 import static java.util.Collections.singleton;
-import javax.annotation.Nullable;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
 
 /**
  * This step must be executed after computation of quality gate measure {@link QualityGateMeasuresStep}
@@ -78,13 +74,7 @@ public class QualityGateEventsStep implements ComputationStep {
     if (analysisMetadataHolder.isPullRequest()) {
       return;
     }
-    new DepthTraversalTypeAwareCrawler(
-      new TypeAwareVisitorAdapter(CrawlerDepthLimit.PROJECT, ComponentVisitor.Order.PRE_ORDER) {
-        @Override
-        public void visitProject(Component project) {
-          executeForProject(project);
-        }
-      }).visit(treeRootHolder.getRoot());
+    executeForProject(treeRootHolder.getRoot());
   }
 
   private void executeForProject(Component project) {
@@ -113,7 +103,7 @@ public class QualityGateEventsStep implements ComputationStep {
 
     if (baseStatus.getStatus() != rawStatus.getStatus()) {
       // The QualityGate status has changed
-      createEvent(project, rawStatus.getStatus().getLabel(), rawStatus.getText());
+      createEvent(rawStatus.getStatus().getLabel(), rawStatus.getText());
       boolean isNewKo = rawStatus.getStatus() == Measure.Level.OK;
       notifyUsers(project, rawStatus, isNewKo);
     }
@@ -122,7 +112,7 @@ public class QualityGateEventsStep implements ComputationStep {
   private void checkNewQualityGate(Component project, QualityGateStatus rawStatus) {
     if (rawStatus.getStatus() != Measure.Level.OK) {
       // There were no defined alerts before, so this one is a new one
-      createEvent(project, rawStatus.getStatus().getLabel(), rawStatus.getText());
+      createEvent(rawStatus.getStatus().getLabel(), rawStatus.getText());
       notifyUsers(project, rawStatus, true);
     }
   }
@@ -155,8 +145,8 @@ public class QualityGateEventsStep implements ComputationStep {
     notificationService.deliver(notification);
   }
 
-  private void createEvent(Component project, String name, @Nullable String description) {
-    eventRepository.add(project, Event.createAlert(name, null, description));
+  private void createEvent(String name, @Nullable String description) {
+    eventRepository.add(Event.createAlert(name, null, description));
   }
 
   @Override
index f859e2bfb24b10914da0d7f89f817b378436d1ee..8d9f13c2baedd1f7558a044602960112f6fe22db 100644 (file)
@@ -30,10 +30,7 @@ import org.sonar.api.measures.CoreMetrics;
 import org.sonar.api.resources.Language;
 import org.sonar.api.utils.KeyValueFormat;
 import org.sonar.ce.task.projectanalysis.component.Component;
-import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
-import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
-import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
 import org.sonar.ce.task.projectanalysis.event.Event;
 import org.sonar.ce.task.projectanalysis.event.EventRepository;
 import org.sonar.ce.task.projectanalysis.language.LanguageRepository;
@@ -46,14 +43,12 @@ import org.sonar.core.util.UtcDateUtils;
 import org.sonar.server.qualityprofile.QPMeasureData;
 import org.sonar.server.qualityprofile.QualityProfile;
 
-import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
 import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.ADDED;
 import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.REMOVED;
 import static org.sonar.ce.task.projectanalysis.qualityprofile.QProfileStatusRepository.Status.UPDATED;
 
 /**
  * Computation of quality profile events
- *
  * As it depends upon {@link CoreMetrics#QUALITY_PROFILES_KEY}, it must be executed after {@link ComputeQProfileMeasureStep}
  */
 public class QualityProfileEventsStep implements ComputationStep {
@@ -62,7 +57,7 @@ public class QualityProfileEventsStep implements ComputationStep {
   private final MeasureRepository measureRepository;
   private final EventRepository eventRepository;
   private final LanguageRepository languageRepository;
-  private QProfileStatusRepository qProfileStatusRepository;
+  private final QProfileStatusRepository qProfileStatusRepository;
 
   public QualityProfileEventsStep(TreeRootHolder treeRootHolder,
     MetricRepository metricRepository, MeasureRepository measureRepository, LanguageRepository languageRepository,
@@ -77,13 +72,7 @@ public class QualityProfileEventsStep implements ComputationStep {
 
   @Override
   public void execute(ComputationStep.Context context) {
-    new DepthTraversalTypeAwareCrawler(
-      new TypeAwareVisitorAdapter(CrawlerDepthLimit.PROJECT, POST_ORDER) {
-        @Override
-        public void visitProject(Component tree) {
-          executeForProject(tree);
-        }
-      }).visit(treeRootHolder.getRoot());
+    executeForProject(treeRootHolder.getRoot());
   }
 
   private void executeForProject(Component projectComponent) {
@@ -102,8 +91,8 @@ public class QualityProfileEventsStep implements ComputationStep {
     Map<String, QualityProfile> rawProfiles = QPMeasureData.fromJson(rawMeasure.get().getStringValue()).getProfilesByKey();
 
     Map<String, QualityProfile> baseProfiles = parseJsonData(baseMeasure.get());
-    detectNewOrUpdatedProfiles(projectComponent, baseProfiles, rawProfiles);
-    detectNoMoreUsedProfiles(projectComponent, baseProfiles);
+    detectNewOrUpdatedProfiles(baseProfiles, rawProfiles);
+    detectNoMoreUsedProfiles(baseProfiles);
   }
 
   private static Map<String, QualityProfile> parseJsonData(Measure measure) {
@@ -114,42 +103,42 @@ public class QualityProfileEventsStep implements ComputationStep {
     return QPMeasureData.fromJson(data).getProfilesByKey();
   }
 
-  private void detectNoMoreUsedProfiles(Component context, Map<String, QualityProfile> baseProfiles) {
+  private void detectNoMoreUsedProfiles(Map<String, QualityProfile> baseProfiles) {
     for (QualityProfile baseProfile : baseProfiles.values()) {
       if (qProfileStatusRepository.get(baseProfile.getQpKey()).filter(REMOVED::equals).isPresent()) {
-        markAsRemoved(context, baseProfile);
+        markAsRemoved(baseProfile);
       }
     }
   }
 
-  private void detectNewOrUpdatedProfiles(Component component, Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles) {
+  private void detectNewOrUpdatedProfiles(Map<String, QualityProfile> baseProfiles, Map<String, QualityProfile> rawProfiles) {
     for (QualityProfile profile : rawProfiles.values()) {
       qProfileStatusRepository.get(profile.getQpKey()).ifPresent(status -> {
         if (status.equals(ADDED)) {
-          markAsAdded(component, profile);
+          markAsAdded(profile);
         } else if (status.equals(UPDATED)) {
-          markAsChanged(component, baseProfiles.get(profile.getQpKey()), profile);
+          markAsChanged(baseProfiles.get(profile.getQpKey()), profile);
         }
       });
     }
   }
 
-  private void markAsChanged(Component component, QualityProfile baseProfile, QualityProfile profile) {
+  private void markAsChanged(QualityProfile baseProfile, QualityProfile profile) {
     Date from = baseProfile.getRulesUpdatedAt();
 
     String data = KeyValueFormat.format(ImmutableSortedMap.of(
       "key", profile.getQpKey(),
       "from", UtcDateUtils.formatDateTime(fixDate(from)),
       "to", UtcDateUtils.formatDateTime(fixDate(profile.getRulesUpdatedAt()))));
-    eventRepository.add(component, createQProfileEvent(profile, "Changes in %s", data));
+    eventRepository.add(createQProfileEvent(profile, "Changes in %s", data));
   }
 
-  private void markAsRemoved(Component component, QualityProfile profile) {
-    eventRepository.add(component, createQProfileEvent(profile, "Stop using %s"));
+  private void markAsRemoved(QualityProfile profile) {
+    eventRepository.add(createQProfileEvent(profile, "Stop using %s"));
   }
 
-  private void markAsAdded(Component component, QualityProfile profile) {
-    eventRepository.add(component, createQProfileEvent(profile, "Use %s"));
+  private void markAsAdded(QualityProfile profile) {
+    eventRepository.add(createQProfileEvent(profile, "Use %s"));
   }
 
   private Event createQProfileEvent(QualityProfile profile, String namePattern) {
index 87c28ad4872f5ce6a5c28306c49705bd6a909556..71441ccc56e2bcf0bf985f2ff4ea8ad22e37d594 100644 (file)
  */
 package org.sonar.ce.task.projectanalysis.event;
 
-import java.util.Arrays;
 import org.junit.Test;
 import org.sonar.ce.task.projectanalysis.component.Component;
 import org.sonar.ce.task.projectanalysis.component.ReportComponent;
-import org.sonar.ce.task.projectanalysis.component.ViewsComponent;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.Assertions.fail;
 
 public class EventRepositoryImplTest {
-  private static final Component COMPONENT_1 = newComponent(1);
-  private static final Component COMPONENT_2 = newComponent(2);
   private static final Event EVENT_1 = Event.createProfile("event_1", null, null);
   private static final Event EVENT_2 = Event.createProfile("event_2", null, null);
 
-  private EventRepositoryImpl underTest = new EventRepositoryImpl();
+  private final EventRepositoryImpl underTest = new EventRepositoryImpl();
 
   @Test
   public void getEvents_returns_empty_iterable_when_repository_is_empty() {
-    assertThat(underTest.getEvents(COMPONENT_1)).isEmpty();
-  }
-
-  @Test
-  public void getEvents_discriminates_per_component() {
-    underTest.add(COMPONENT_1, EVENT_1);
-    underTest.add(COMPONENT_2, EVENT_2);
-
-    assertThat(underTest.getEvents(COMPONENT_1)).extracting("name").containsExactly(EVENT_1.getName());
-    assertThat(underTest.getEvents(COMPONENT_2)).extracting("name").containsExactly(EVENT_2.getName());
-  }
-
-  @Test
-  public void add_throws_NPE_if_component_arg_is_null() {
-    assertThatThrownBy(() -> underTest.add(null, EVENT_1))
-      .isInstanceOf(NullPointerException.class);
+    assertThat(underTest.getEvents()).isEmpty();
   }
 
   @Test
   public void add_throws_NPE_if_even_arg_is_null() {
-    assertThatThrownBy(() -> underTest.add(COMPONENT_1, null))
+    assertThatThrownBy(() -> underTest.add(null))
       .isInstanceOf(NullPointerException.class);
   }
 
   @Test
-  public void add_throws_IAE_for_any_component_type_but_PROJECT() {
-    Arrays.stream(Component.Type.values())
-      .filter(type -> type != Component.Type.PROJECT)
-      .map(type -> {
-        if (type.isReportType()) {
-          return ReportComponent.builder(type, 1).build();
-        } else {
-          return ViewsComponent.builder(type, 1).build();
-        }
-      })
-      .forEach(component -> {
-        try {
-          underTest.add(component, EVENT_1);
-          fail("should have raised an IAE");
-        } catch (IllegalArgumentException e) {
-          assertThat(e).hasMessage("Component must be of type PROJECT");
-        }
-      });
-  }
-
-  @Test
-  public void can_add_and_retrieve_many_events_per_component() {
-    underTest.add(COMPONENT_1, EVENT_1);
-    underTest.add(COMPONENT_1, EVENT_2);
+  public void can_add_and_retrieve_many_events() {
+    underTest.add(EVENT_1);
+    underTest.add(EVENT_2);
 
-    assertThat(underTest.getEvents(COMPONENT_1)).extracting("name").containsOnly(EVENT_1.getName(), EVENT_2.getName());
+    assertThat(underTest.getEvents()).extracting("name").containsOnly(EVENT_1.getName(), EVENT_2.getName());
   }
 
   private static Component newComponent(int i) {
index 56a656ba4dbc2ff907c46564897e0cd25249c0c0..3121eb823929dc7a83e3c9cfa15fa57fb1f6404e 100644 (file)
@@ -171,7 +171,7 @@ public class QualityGateEventsStepTest {
 
     verify(measureRepository).getRawMeasure(PROJECT_COMPONENT, alertStatusMetric);
     verify(measureRepository).getBaseMeasure(PROJECT_COMPONENT, alertStatusMetric);
-    verify(eventRepository).add(eq(PROJECT_COMPONENT), eventArgumentCaptor.capture());
+    verify(eventRepository).add(eventArgumentCaptor.capture());
     verifyNoMoreInteractions(measureRepository, eventRepository);
 
     Event event = eventArgumentCaptor.getValue();
@@ -227,7 +227,7 @@ public class QualityGateEventsStepTest {
 
     verify(measureRepository).getRawMeasure(PROJECT_COMPONENT, alertStatusMetric);
     verify(measureRepository).getBaseMeasure(PROJECT_COMPONENT, alertStatusMetric);
-    verify(eventRepository).add(eq(PROJECT_COMPONENT), eventArgumentCaptor.capture());
+    verify(eventRepository).add(eventArgumentCaptor.capture());
     verifyNoMoreInteractions(measureRepository, eventRepository);
 
     Event event = eventArgumentCaptor.getValue();
index 31d1c8fac2d128c51061ea2410707604d069e894..5bf804c0007ffa2a49ed572d05cf131771cbd5b2 100644 (file)
@@ -132,7 +132,7 @@ public class QualityProfileEventsStepTest {
 
     underTest.execute(new TestComputationStepContext());
 
-    verify(eventRepository).add(eq(treeRootHolder.getRoot()), eventArgumentCaptor.capture());
+    verify(eventRepository).add(eventArgumentCaptor.capture());
     verifyNoMoreInteractions(eventRepository);
     verifyEvent(eventArgumentCaptor.getValue(), "Use '" + qp.getQpName() + "' (" + language.getName() + ")", null);
   }
@@ -147,7 +147,7 @@ public class QualityProfileEventsStepTest {
 
     underTest.execute(new TestComputationStepContext());
 
-    verify(eventRepository).add(eq(treeRootHolder.getRoot()), eventArgumentCaptor.capture());
+    verify(eventRepository).add(eventArgumentCaptor.capture());
     verifyNoMoreInteractions(eventRepository);
     verifyEvent(eventArgumentCaptor.getValue(), "Use '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null);
   }
@@ -162,7 +162,7 @@ public class QualityProfileEventsStepTest {
 
     underTest.execute(new TestComputationStepContext());
 
-    verify(eventRepository).add(eq(treeRootHolder.getRoot()), eventArgumentCaptor.capture());
+    verify(eventRepository).add(eventArgumentCaptor.capture());
     verifyNoMoreInteractions(eventRepository);
     verifyEvent(eventArgumentCaptor.getValue(), "Stop using '" + qp.getQpName() + "' (" + language.getName() + ")", null);
   }
@@ -176,7 +176,7 @@ public class QualityProfileEventsStepTest {
 
     underTest.execute(new TestComputationStepContext());
 
-    verify(eventRepository).add(eq(treeRootHolder.getRoot()), eventArgumentCaptor.capture());
+    verify(eventRepository).add(eventArgumentCaptor.capture());
     verifyNoMoreInteractions(eventRepository);
     verifyEvent(eventArgumentCaptor.getValue(), "Stop using '" + qp.getQpName() + "' (" + qp.getLanguageKey() + ")", null);
   }
@@ -189,7 +189,7 @@ public class QualityProfileEventsStepTest {
 
     underTest.execute(new TestComputationStepContext());
 
-    verify(eventRepository, never()).add(any(Component.class), any(Event.class));
+    verify(eventRepository, never()).add(any(Event.class));
   }
 
   @Test
@@ -202,7 +202,7 @@ public class QualityProfileEventsStepTest {
 
     underTest.execute(new TestComputationStepContext());
 
-    verify(eventRepository).add(eq(treeRootHolder.getRoot()), eventArgumentCaptor.capture());
+    verify(eventRepository).add(eventArgumentCaptor.capture());
     verifyNoMoreInteractions(eventRepository);
     verifyEvent(eventArgumentCaptor.getValue(),
       "Changes in '" + qp2.getQpName() + "' (" + language.getName() + ")",
@@ -214,9 +214,9 @@ public class QualityProfileEventsStepTest {
   public void verify_detection_with_complex_mix_of_qps() {
     final Set<Event> events = new HashSet<>();
     doAnswer(invocationOnMock -> {
-      events.add((Event) invocationOnMock.getArguments()[1]);
+      events.add((Event) invocationOnMock.getArguments()[0]);
       return null;
-    }).when(eventRepository).add(eq(treeRootHolder.getRoot()), any(Event.class));
+    }).when(eventRepository).add(any(Event.class));
 
     Date date = new Date();
     QualityProfile qp1 = qp(QP_NAME_2, LANGUAGE_KEY_1, date);
index cf3916b45ee60360562fc6e9c4925da365e05470..b0bf23234adb8e191ac74291a7a70f10bfc366fa 100644 (file)
@@ -361,7 +361,7 @@ public class SnapshotDaoIT {
     SnapshotDto oldAnalysisOnThirdProject = db.components().insertSnapshot(thirdProject, s -> s.setStatus(STATUS_PROCESSED).setCreatedAt(otherFrom - 1L));
     insertActivity(thirdProject.uuid(), oldAnalysisOnThirdProject, SUCCESS);
 
-    List<SnapshotDto> result = underTest.selectFinishedByComponentUuidsAndFromDates(dbSession,
+    List<SnapshotDto> result = underTest.selectFinishedByProjectUuidsAndFromDates(dbSession,
       Arrays.asList(firstProject.uuid(), secondProject.uuid(), thirdProject.uuid()),
       Arrays.asList(from, otherFrom, otherFrom));
 
@@ -380,7 +380,7 @@ public class SnapshotDaoIT {
     SnapshotDto canceledAnalysis = db.components().insertSnapshot(project, s -> s.setStatus(STATUS_PROCESSED).setCreatedAt(from));
     insertActivity(project.uuid(), canceledAnalysis, CANCELED);
 
-    List<SnapshotDto> result = underTest.selectFinishedByComponentUuidsAndFromDates(dbSession, singletonList(project.uuid()), singletonList(from));
+    List<SnapshotDto> result = underTest.selectFinishedByProjectUuidsAndFromDates(dbSession, singletonList(project.uuid()), singletonList(from));
 
     assertThat(result).extracting(SnapshotDto::getUuid)
       .containsExactlyInAnyOrder(finishedAnalysis.getUuid(), canceledAnalysis.getUuid());
@@ -401,7 +401,7 @@ public class SnapshotDaoIT {
     SnapshotDto analysisOnSecondBranch = db.components().insertSnapshot(secondBranch, s -> s.setStatus(STATUS_PROCESSED).setCreatedAt(from));
     insertActivity(project.uuid(), analysisOnSecondBranch, SUCCESS);
 
-    List<SnapshotDto> result = underTest.selectFinishedByComponentUuidsAndFromDates(dbSession, singletonList(project.uuid()), singletonList(from));
+    List<SnapshotDto> result = underTest.selectFinishedByProjectUuidsAndFromDates(dbSession, singletonList(project.uuid()), singletonList(from));
 
     assertThat(result).extracting(SnapshotDto::getUuid)
       .containsExactlyInAnyOrder(finishedAnalysis.getUuid(), otherFinishedAnalysis.getUuid(), analysisOnSecondBranch.getUuid());
index 2dc669270dfbc0307bd371477dfb21c32fd105fb..1d8b0ef5b6af1882174b29228fcd126049e64547 100644 (file)
@@ -100,15 +100,15 @@ public class SnapshotDao implements Dao {
    *
    * Note that branches analysis of projects are also returned.
    */
-  public List<SnapshotDto> selectFinishedByComponentUuidsAndFromDates(DbSession dbSession, List<String> componentUuids, List<Long> fromDates) {
-    checkArgument(componentUuids.size() == fromDates.size(), "The number of components (%s) and from dates (%s) must be the same.",
-      String.valueOf(componentUuids.size()),
+  public List<SnapshotDto> selectFinishedByProjectUuidsAndFromDates(DbSession dbSession, List<String> projectUuids, List<Long> fromDates) {
+    checkArgument(projectUuids.size() == fromDates.size(), "The number of components (%s) and from dates (%s) must be the same.",
+      String.valueOf(projectUuids.size()),
       String.valueOf(fromDates.size()));
-    List<ComponentUuidFromDatePair> componentUuidFromDatePairs = IntStream.range(0, componentUuids.size())
-      .mapToObj(i -> new ComponentUuidFromDatePair(componentUuids.get(i), fromDates.get(i)))
-      .collect(MoreCollectors.toList(componentUuids.size()));
+    List<ComponentUuidFromDatePair> componentUuidFromDatePairs = IntStream.range(0, projectUuids.size())
+      .mapToObj(i -> new ComponentUuidFromDatePair(projectUuids.get(i), fromDates.get(i)))
+      .collect(MoreCollectors.toList(projectUuids.size()));
 
-    return executeLargeInputs(componentUuidFromDatePairs, partition -> mapper(dbSession).selectFinishedByComponentUuidsAndFromDates(partition), i -> i / 2);
+    return executeLargeInputs(componentUuidFromDatePairs, partition -> mapper(dbSession).selectFinishedByProjectUuidsAndFromDates(partition), i -> i / 2);
   }
 
   public void switchIsLastFlagAndSetProcessedStatus(DbSession dbSession, String componentUuid, String analysisUuid) {
index cee720f36a28934b76741606678b285691fa07cd..a28063c7b78a3dee751c0aaa8df3a591a9c29ce8 100644 (file)
@@ -52,7 +52,7 @@ public interface SnapshotMapper {
 
   void update(SnapshotDto analysis);
 
-  List<SnapshotDto> selectFinishedByComponentUuidsAndFromDates(@Param("componentUuidFromDatePairs") List<ComponentUuidFromDatePair> pairs);
+  List<SnapshotDto> selectFinishedByProjectUuidsAndFromDates(@Param("projectUuidFromDatePairs") List<ComponentUuidFromDatePair> pairs);
 
   @CheckForNull
   Long selectLastAnalysisDateByProject(String projectUuid);
index 04d71b3fc177f5eae8c591d4197b3bf609840f34..7d2adfa6b13f6d853d5a40522e34a59ae03b78b0 100644 (file)
     </if>
   </select>
 
-  <select id="selectFinishedByComponentUuidsAndFromDates" parameterType="map" resultType="Snapshot">
+  <select id="selectFinishedByProjectUuidsAndFromDates" parameterType="map" resultType="Snapshot">
     select
     <include refid="snapshotColumns" />
     from snapshots s
       inner join components p on p.uuid=s.component_uuid and p.enabled=${_true}
       inner join project_branches pb on pb.uuid=p.uuid
     where
-      <foreach collection="componentUuidFromDatePairs" open="(" close=")" item="componentUuidFromDatePair" separator=" or ">
-        (pb.project_uuid=#{componentUuidFromDatePair.componentUuid, jdbcType=VARCHAR} and s.created_at >= #{componentUuidFromDatePair.from, jdbcType=BIGINT})
+      <foreach collection="projectUuidFromDatePairs" open="(" close=")" item="projectUuidFromDatePair" separator=" or ">
+        (pb.project_uuid=#{projectUuidFromDatePair.componentUuid, jdbcType=VARCHAR} and s.created_at >= #{projectUuidFromDatePair.from, jdbcType=BIGINT})
       </foreach>
       and s.status = 'P'
     order by
index 61c0b37bfba0fc8584c49225ca2755157b37b494..b4028725c2909bcb3da1f94cd84efa058da0f8fb 100644 (file)
@@ -33,6 +33,7 @@ import org.sonar.db.ce.CeQueueDto;
 import org.sonar.db.ce.CeTaskTypes;
 import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ProjectData;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.server.es.EsTester;
@@ -86,7 +87,7 @@ public class SearchEventsActionNewIssuesIT {
     userSession.logIn();
     when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto analysis = insertAnalysis(project, 1_500_000_000_000L);
     insertIssue(project, analysis);
     insertIssue(project, analysis);
@@ -113,21 +114,22 @@ public class SearchEventsActionNewIssuesIT {
   public void many_issues_events() {
     userSession.logIn();
     long from = 1_500_000_000_000L;
-    ComponentDto project = db.components().insertPrivateProject(p -> p.setName("SonarQube")).getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
-    SnapshotDto analysis = insertAnalysis(project, from);
-    insertIssue(project, analysis);
-    insertIssue(project, analysis);
+    ProjectData projectData = db.components().insertPrivateProject(p -> p.setName("SonarQube"));
+    ComponentDto mainBranchComponent = projectData.getMainBranchComponent();
+    userSession.addProjectPermission(USER, projectData.getProjectDto());
+    SnapshotDto analysis = insertAnalysis(mainBranchComponent, from);
+    insertIssue(mainBranchComponent, analysis);
+    insertIssue(mainBranchComponent, analysis);
     issueIndexer.indexAllIssues();
     String fromDate = formatDateTime(from - 1_000L);
 
     SearchEventsWsResponse result = ws.newRequest()
-      .setParam(PARAM_PROJECTS, project.getKey())
+      .setParam(PARAM_PROJECTS, mainBranchComponent.getKey())
       .setParam(PARAM_FROM, fromDate)
       .executeProtobuf(SearchEventsWsResponse.class);
 
     assertThat(result.getEventsList()).extracting(Event::getCategory, Event::getMessage, Event::getProject, Event::getDate)
-      .containsExactly(tuple("NEW_ISSUES", "You have 2 new issues on project 'SonarQube'", project.getKey(),
+      .containsExactly(tuple("NEW_ISSUES", "You have 2 new issues on project 'SonarQube'", mainBranchComponent.getKey(),
         formatDateTime(from)));
   }
 
@@ -152,7 +154,7 @@ public class SearchEventsActionNewIssuesIT {
   public void return_link_to_issue_search_for_new_issues_event() {
     userSession.logIn("my_login");
     ComponentDto project = db.components().insertPrivateProject(p -> p.setKey("my_project")).getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto analysis = insertAnalysis(project, 1_400_000_000_000L);
     insertIssue(project, analysis);
     issueIndexer.indexAllIssues();
@@ -172,7 +174,7 @@ public class SearchEventsActionNewIssuesIT {
     userSession.logIn().setSystemAdministrator();
     when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     String branchName1 = "branch1";
     ComponentDto branch1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH).setKey(branchName1));
     SnapshotDto branch1Analysis = insertAnalysis(branch1, project.uuid(), 1_500_000_000_000L);
@@ -208,7 +210,7 @@ public class SearchEventsActionNewIssuesIT {
     userSession.logIn().setSystemAdministrator();
     when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     String nonMainBranchName = "nonMain";
     ComponentDto nonMainBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH).setKey(nonMainBranchName));
     SnapshotDto nonMainBranchAnalysis = insertAnalysis(nonMainBranch, project.uuid(), 1_500_000_000_000L);
@@ -245,7 +247,7 @@ public class SearchEventsActionNewIssuesIT {
     userSession.logIn("rågnar").setSystemAdministrator();
     long from = 1_500_000_000_000L;
     ComponentDto project = db.components().insertPrivateProject(p -> p.setKey("M&M's")).getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto analysis = insertAnalysis(project, from);
     insertIssue(project, analysis);
     issueIndexer.indexAllIssues();
index 45550366d62e8b244e09a6774791b23af99fc120..2d59cbe82e562252f1154689c8bfa3aebee21615 100644 (file)
@@ -64,17 +64,17 @@ public class SearchEventsActionQualityGateIT {
 
   private static final long ANY_TIMESTAMP = 1666666666L;
 
-  private Server server = mock(Server.class);
-  private IssueIndex issueIndex = new IssueIndex(es.client(), null, userSession, null);
-  private IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
-  private WsActionTester ws = new WsActionTester(new SearchEventsAction(db.getDbClient(), userSession, server, issueIndex,
+  private final Server server = mock(Server.class);
+  private final IssueIndex issueIndex = new IssueIndex(es.client(), null, userSession, null);
+  private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
+  private final WsActionTester ws = new WsActionTester(new SearchEventsAction(db.getDbClient(), userSession, server, issueIndex,
     issueIndexSyncProgressChecker));
 
   @Test
   public void quality_gate_events() {
     when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto projectAnalysis = insertSuccessfulActivity(project, 1_500_000_000_000L);
     db.events().insertEvent(newQualityGateEvent(projectAnalysis).setDate(projectAnalysis.getCreatedAt()).setName("Failed"));
 
@@ -98,7 +98,7 @@ public class SearchEventsActionQualityGateIT {
   public void branch_quality_gate_events() {
     when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     String branchName = randomAlphanumeric(248);
     ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH).setKey(branchName));
     insertSuccessfulActivity(project, 1_500_000_000_000L);
@@ -141,7 +141,7 @@ public class SearchEventsActionQualityGateIT {
   @Test
   public void return_only_latest_quality_gate_event() {
     ComponentDto project = db.components().insertPrivateProject(p -> p.setName("My Project")).getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto a1 = insertSuccessfulActivity(project, 1_500_000_000_000L);
     EventDto e1 = db.events().insertEvent(newQualityGateEvent(a1).setName("Failed").setDate(a1.getCreatedAt()));
     SnapshotDto a2 = insertSuccessfulActivity(project, 1_500_000_000_001L);
@@ -159,7 +159,7 @@ public class SearchEventsActionQualityGateIT {
   @Test
   public void return_link_to_dashboard_for_quality_gate_event() {
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto analysis = insertSuccessfulActivity(project, 1_500_000_000_000L);
     EventDto e1 = db.events().insertEvent(newQualityGateEvent(analysis).setName("Failed").setDate(analysis.getCreatedAt()));
     when(server.getPublicRootUrl()).thenReturn("https://sonarcloud.io");
@@ -176,7 +176,7 @@ public class SearchEventsActionQualityGateIT {
   @Test
   public void encode_link() {
     ComponentDto project = db.components().insertPrivateProject(p -> p.setKey("M&M's")).getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto analysis = insertSuccessfulActivity(project, 1_500_000_000_000L);
     EventDto event = db.events().insertEvent(newQualityGateEvent(analysis).setName("Failed").setDate(analysis.getCreatedAt()));
     when(server.getPublicRootUrl()).thenReturn("http://sonarcloud.io");
@@ -193,7 +193,7 @@ public class SearchEventsActionQualityGateIT {
   @Test
   public void filter_quality_gate_event() {
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project));
     SnapshotDto analysis = insertSuccessfulActivity(project, 1_500_000_000_000L);
     EventDto qualityGateEvent = db.events().insertEvent(newQualityGateEvent(analysis).setDate(analysis.getCreatedAt()));
     EventDto versionEvent = db.events().insertEvent(newEvent(analysis).setCategory(EventCategory.VERSION.getLabel()).setDate(analysis.getCreatedAt()));
@@ -211,11 +211,11 @@ public class SearchEventsActionQualityGateIT {
   @Test
   public void filter_by_from_date_inclusive() {
     ComponentDto project1 = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project1);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project1));
     ComponentDto project2 = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project2);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project2));
     ComponentDto project3 = db.components().insertPrivateProject().getMainBranchComponent();
-    userSession.addProjectPermission(USER, project3);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project3));
     long from1 = 1_500_000_000_000L;
     long from2 = 1_400_000_000_000L;
     long from3 = 1_300_000_000_000L;
@@ -239,9 +239,9 @@ public class SearchEventsActionQualityGateIT {
   @Test
   public void return_one_quality_gate_change_per_project() {
     ComponentDto project1 = db.components().insertPrivateProject(p -> p.setName("p1")).getMainBranchComponent();
-    userSession.addProjectPermission(USER, project1);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project1));
     ComponentDto project2 = db.components().insertPrivateProject(p -> p.setName("p2")).getMainBranchComponent();
-    userSession.addProjectPermission(USER, project2);
+    userSession.addProjectPermission(USER, db.components().getProjectDtoByMainBranch(project2));
     long from = 1_500_000_000_000L;
     SnapshotDto a11 = insertSuccessfulActivity(project1, from);
     SnapshotDto a12 = insertSuccessfulActivity(project1, from + 1L);
index b52267876b7f2fe5fb2fe6ab55728ab406d04428..1f965dc99860e7df8d7838b04a784771c61605d6 100644 (file)
@@ -24,6 +24,7 @@ import java.net.URLEncoder;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
@@ -134,8 +135,8 @@ public class SearchEventsAction implements DevelopersWsAction {
     try (DbSession dbSession = dbClient.openSession(false)) {
       List<ProjectDto> authorizedProjects = searchProjects(dbSession, projectKeys);
       Map<String, ProjectDto> projectsByUuid = authorizedProjects.stream().collect(uniqueIndex(ProjectDto::getUuid));
-      List<UuidFromPair> uuidFromPairs = componentUuidFromPairs(fromDates, projectKeys, authorizedProjects);
-      List<SnapshotDto> analyses = dbClient.snapshotDao().selectFinishedByComponentUuidsAndFromDates(dbSession, componentUuids(uuidFromPairs), fromDates(uuidFromPairs));
+      List<UuidFromPair> uuidFromPairs = buildUuidFromPairs(fromDates, projectKeys, authorizedProjects);
+      List<SnapshotDto> analyses = dbClient.snapshotDao().selectFinishedByProjectUuidsAndFromDates(dbSession, componentUuids(uuidFromPairs), fromDates(uuidFromPairs));
 
       if (analyses.isEmpty()) {
         return Stream.empty();
@@ -224,7 +225,7 @@ public class SearchEventsAction implements DevelopersWsAction {
     return link;
   }
 
-  private static List<UuidFromPair> componentUuidFromPairs(List<Long> fromDates, List<String> projectKeys, List<ProjectDto> authorizedProjects) {
+  private static List<UuidFromPair> buildUuidFromPairs(List<Long> fromDates, List<String> projectKeys, List<ProjectDto> authorizedProjects) {
     checkRequest(projectKeys.size() == fromDates.size(), "The number of components (%s) and from dates (%s) must be the same.", projectKeys.size(), fromDates.size());
     Map<String, Long> fromDatesByProjectKey = IntStream.range(0, projectKeys.size()).boxed()
       .collect(uniqueIndex(projectKeys::get, fromDates::get));