Add AttentionSetListener Interface
Adds a new AttentionSetListener and causes it to fire when an
appropriate server Op is called
Change-Id: Ifc8c2bb12b21f72a5ea5126bef01f06ea550d35e
Release-Notes: Adds "Add/Removed from Attention Set" events to API
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 0acf3bc..3d90bf0 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.events.AccountIndexedListener;
+import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
@@ -103,6 +104,7 @@
private final DynamicSet<OnPostReview> onPostReviews;
private final DynamicSet<ReviewerAddedListener> reviewerAddedListeners;
private final DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners;
+ private final DynamicSet<AttentionSetListener> attentionSetListeners;
private final DynamicMap<ChangeHasOperandFactory> hasOperands;
private final DynamicMap<ChangeIsOperandFactory> isOperands;
@@ -147,7 +149,8 @@
DynamicSet<ReviewerAddedListener> reviewerAddedListeners,
DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners,
DynamicMap<ChangeHasOperandFactory> hasOperands,
- DynamicMap<ChangeIsOperandFactory> isOperands) {
+ DynamicMap<ChangeIsOperandFactory> isOperands,
+ DynamicSet<AttentionSetListener> attentionSetListeners) {
this.accountIndexedListeners = accountIndexedListeners;
this.changeIndexedListeners = changeIndexedListeners;
this.groupIndexedListeners = groupIndexedListeners;
@@ -187,6 +190,7 @@
this.reviewerDeletedListeners = reviewerDeletedListeners;
this.hasOperands = hasOperands;
this.isOperands = isOperands;
+ this.attentionSetListeners = attentionSetListeners;
}
public Registration newRegistration() {
@@ -330,6 +334,10 @@
return add(workInProgressStateChangedListeners, workInProgressStateChangedListener);
}
+ public Registration add(AttentionSetListener attentionSetListener) {
+ return add(attentionSetListeners, attentionSetListener);
+ }
+
public Registration add(CapabilityDefinition capabilityDefinition, String exportName) {
return add(capabilityDefinitions, capabilityDefinition, exportName);
}
diff --git a/java/com/google/gerrit/extensions/events/AttentionSetListener.java b/java/com/google/gerrit/extensions/events/AttentionSetListener.java
new file mode 100644
index 0000000..ada30ce
--- /dev/null
+++ b/java/com/google/gerrit/extensions/events/AttentionSetListener.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.Set;
+
+/** Notified whenever the attention set is changed. */
+@ExtensionPoint
+public interface AttentionSetListener {
+ interface Event extends ChangeEvent {
+
+ /**
+ * Returns the users added to the attention set because of this change
+ *
+ * @return Account IDs
+ */
+ Set<Integer> usersAdded();
+
+ /**
+ * Returns the users removed from the attention set because of this change
+ *
+ * @return Account IDs
+ */
+ Set<Integer> usersRemoved();
+ }
+
+ /**
+ * This function will be called when the attention set changes
+ *
+ * @param event The event that changed the attention set
+ */
+ void onAttentionSetChanged(Event event);
+}
diff --git a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
index 1cf31c1..1acc91d 100644
--- a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
@@ -20,6 +20,7 @@
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.extensions.events.AttentionSetObserver;
import com.google.gerrit.server.mail.send.AddToAttentionSetSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.query.change.ChangeData;
@@ -39,6 +40,7 @@
private final ChangeData.Factory changeDataFactory;
private final AddToAttentionSetSender.Factory addToAttentionSetSender;
private final AttentionSetEmail.Factory attentionSetEmailFactory;
+ private final AttentionSetObserver attentionSetObserver;
private final Account.Id attentionUserId;
private final String reason;
@@ -58,12 +60,14 @@
ChangeData.Factory changeDataFactory,
AddToAttentionSetSender.Factory addToAttentionSetSender,
AttentionSetEmail.Factory attentionSetEmailFactory,
+ AttentionSetObserver attentionSetObserver,
@Assisted Account.Id attentionUserId,
@Assisted String reason,
@Assisted boolean notify) {
this.changeDataFactory = changeDataFactory;
this.addToAttentionSetSender = addToAttentionSetSender;
this.attentionSetEmailFactory = attentionSetEmailFactory;
+ this.attentionSetObserver = attentionSetObserver;
this.attentionUserId = requireNonNull(attentionUserId, "user");
this.reason = requireNonNull(reason, "reason");
@@ -85,10 +89,13 @@
change = ctx.getChange();
- ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
- update.addToPlannedAttentionSetUpdates(
+ ChangeUpdate changeUpdate = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ AttentionSetUpdate attentionUpdate =
AttentionSetUpdate.createForWrite(
- attentionUserId, AttentionSetUpdate.Operation.ADD, reason));
+ attentionUserId, AttentionSetUpdate.Operation.ADD, reason);
+ changeUpdate.addToPlannedAttentionSetUpdates(attentionUpdate);
+ attentionSetObserver.fire(
+ changeDataFactory.create(change), ctx.getAccount(), attentionUpdate, ctx.getWhen());
return true;
}
diff --git a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
index 9fb4fc4..2305791 100644
--- a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
@@ -21,6 +21,7 @@
import com.google.gerrit.entities.AttentionSetUpdate.Operation;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.extensions.events.AttentionSetObserver;
import com.google.gerrit.server.mail.send.RemoveFromAttentionSetSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.query.change.ChangeData;
@@ -41,6 +42,7 @@
private final ChangeData.Factory changeDataFactory;
private final RemoveFromAttentionSetSender.Factory removeFromAttentionSetSender;
private final AttentionSetEmail.Factory attentionSetEmailFactory;
+ private final AttentionSetObserver attentionSetObserver;
private final Account.Id attentionUserId;
private final String reason;
@@ -60,12 +62,14 @@
ChangeData.Factory changeDataFactory,
RemoveFromAttentionSetSender.Factory removeFromAttentionSetSenderFactory,
AttentionSetEmail.Factory attentionSetEmailFactory,
+ AttentionSetObserver attentionSetObserver,
@Assisted Account.Id attentionUserId,
@Assisted String reason,
@Assisted boolean notify) {
this.changeDataFactory = changeDataFactory;
this.removeFromAttentionSetSender = removeFromAttentionSetSenderFactory;
this.attentionSetEmailFactory = attentionSetEmailFactory;
+ this.attentionSetObserver = attentionSetObserver;
this.attentionUserId = requireNonNull(attentionUserId, "user");
this.reason = requireNonNull(reason, "reason");
this.notify = notify;
@@ -86,9 +90,12 @@
change = ctx.getChange();
- ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
- update.addToPlannedAttentionSetUpdates(
- AttentionSetUpdate.createForWrite(attentionUserId, Operation.REMOVE, reason));
+ ChangeUpdate changeUpdate = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ AttentionSetUpdate attentionUpdate =
+ AttentionSetUpdate.createForWrite(attentionUserId, Operation.REMOVE, reason);
+ changeUpdate.addToPlannedAttentionSetUpdates(attentionUpdate);
+ attentionSetObserver.fire(
+ changeDataFactory.create(change), ctx.getAccount(), attentionUpdate, ctx.getWhen());
return true;
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index a750d8e..fa381ad 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -37,6 +37,7 @@
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.events.AgreementSignupListener;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
+import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeDeletedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
@@ -454,6 +455,7 @@
DynamicSet.setOf(binder(), MailSoyTemplateProvider.class);
DynamicSet.setOf(binder(), OnPostReview.class);
DynamicMap.mapOf(binder(), AccountTagProvider.class);
+ DynamicSet.setOf(binder(), AttentionSetListener.class);
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
new file mode 100644
index 0000000..8f51e13
--- /dev/null
+++ b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.extensions.events;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.events.AttentionSetListener;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Helper class to fire an event when an attention set changes. */
+@Singleton
+public class AttentionSetObserver {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final PluginSetContext<AttentionSetListener> listeners;
+ private final EventUtil util;
+ private final AccountCache accountCache;
+
+ @Inject
+ AttentionSetObserver(
+ PluginSetContext<AttentionSetListener> listeners, EventUtil util, AccountCache accountCache) {
+ this.listeners = listeners;
+ this.util = util;
+ this.accountCache = accountCache;
+ }
+
+ /**
+ * Notify all listening plugins
+ *
+ * @param changeData is current data of the change
+ * @param accountState is the initiator of the change
+ * @param update is the update that caused the event
+ * @param when is the time of the event
+ */
+ public void fire(
+ ChangeData changeData, AccountState accountState, AttentionSetUpdate update, Instant when) {
+ if (listeners.isEmpty()) {
+ return;
+ }
+ AccountState target = accountCache.get(update.account()).get();
+
+ HashSet<Integer> added = new HashSet<>();
+ HashSet<Integer> removed = new HashSet<>();
+ switch (update.operation()) {
+ case ADD:
+ added.add(target.account().id().get());
+ break;
+ case REMOVE:
+ removed.add(target.account().id().get());
+ break;
+ }
+
+ try {
+ Event event =
+ new Event(
+ util.changeInfo(changeData), util.accountInfo(accountState), added, removed, when);
+ listeners.runEach(l -> l.onAttentionSetChanged(event));
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log("Exception while firing AttentionSet changed event");
+ }
+ }
+
+ /** Event to be fired when an attention set changes */
+ private static class Event extends AbstractChangeEvent implements AttentionSetListener.Event {
+ private final Set<Integer> added;
+ private final Set<Integer> removed;
+
+ Event(
+ ChangeInfo change,
+ AccountInfo editor,
+ Set<Integer> added,
+ Set<Integer> removed,
+ Instant when) {
+ super(change, editor, when, NotifyHandling.ALL);
+ this.added = added;
+ this.removed = removed;
+ }
+
+ @Override
+ public Set<Integer> usersAdded() {
+ return added;
+ }
+
+ @Override
+ public Set<Integer> usersRemoved() {
+ return removed;
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index c5f0d23..e8ef187 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -83,6 +83,7 @@
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.UseTimezone;
import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
+import com.google.gerrit.acceptance.api.change.ChangeIT.TestAttentionSetListenerModule.TestAttentionSetListener;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
@@ -147,7 +148,9 @@
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
+import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -1536,6 +1539,39 @@
}
@Test
+ public void attentionSetListener_firesOnChange() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ AttentionSetInput addUser = new AttentionSetInput(user.email(), "some reason");
+ TestAttentionSetListener attentionSetListener = new TestAttentionSetListener();
+
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(attentionSetListener)) {
+
+ gApi.changes().id(r1.getChangeId()).addReviewer(user.email());
+ gApi.changes().id(r1.getChangeId()).addToAttentionSet(addUser);
+
+ assertThat(attentionSetListener.fired).isTrue();
+ assertThat(attentionSetListener.lastEvent.usersAdded().size()).isEqualTo(1);
+ attentionSetListener
+ .lastEvent
+ .usersAdded()
+ .forEach(u -> assertThat(u).isEqualTo(user.id().get()));
+ assertThat(attentionSetListener.lastEvent.usersRemoved()).isEmpty();
+
+ attentionSetListener.fired = false;
+ gApi.changes().id(r1.getChangeId()).attention(user.username()).remove(addUser);
+
+ assertThat(attentionSetListener.fired).isTrue();
+ assertThat(attentionSetListener.lastEvent.usersAdded()).isEmpty();
+ assertThat(attentionSetListener.lastEvent.usersRemoved().size()).isEqualTo(1);
+ attentionSetListener
+ .lastEvent
+ .usersRemoved()
+ .forEach(u -> assertThat(u).isEqualTo(user.id().get()));
+ }
+ }
+
+ @Test
public void rebaseChangeBase() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
@@ -4752,6 +4788,27 @@
}
}
+ public static class TestAttentionSetListenerModule extends AbstractModule {
+ @Override
+ public void configure() {
+ DynamicSet.bind(binder(), AttentionSetListener.class).to(TestAttentionSetListener.class);
+ }
+
+ public static class TestAttentionSetListener implements AttentionSetListener {
+ Event lastEvent;
+ boolean fired;
+
+ @Inject
+ public TestAttentionSetListener() {}
+
+ @Override
+ public void onAttentionSetChanged(Event event) {
+ fired = true;
+ lastEvent = event;
+ }
+ }
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}