From 27b133bea8f1032dd87bc1935b9cb706f5b0079b Mon Sep 17 00:00:00 2001 From: Gustaf Lundh Date: Tue, 20 Sep 2016 17:17:41 +0200 Subject: [PATCH] AssigneeChanged event Fired when a change's assignee has been changed or removed: type: "assignee-changed" change: The updated change changer: The user that issued the assignment oldAssignee: Assignee before it was changed. eventCreatedOn: Timestamp describing when this event was created. Change-Id: I5bf1f3f71b84afda82ecb1058a9cbf04185474f3 --- Documentation/cmd-stream-events.txt | 16 +++- .../events/AssigneeChangedListener.java | 29 ++++++ .../gerrit/server/change/DeleteAssignee.java | 18 +++- .../gerrit/server/change/SetAssigneeOp.java | 24 +++-- .../server/config/GerritGlobalModule.java | 2 + .../gerrit/server/data/ChangeAttribute.java | 1 + .../server/events/AssigneeChangedEvent.java | 29 ++++++ .../gerrit/server/events/EventFactory.java | 1 + .../gerrit/server/events/EventTypes.java | 1 + .../events/StreamEventsApiListener.java | 24 ++++- .../extensions/events/AssigneeChanged.java | 91 +++++++++++++++++++ 11 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/events/AssigneeChangedEvent.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt index 1cfb8b94d9..8ce7d7e40f 100644 --- a/Documentation/cmd-stream-events.txt +++ b/Documentation/cmd-stream-events.txt @@ -1,5 +1,4 @@ = gerrit stream-events - == NAME gerrit stream-events - Monitor events occurring in real time @@ -59,6 +58,21 @@ this JSON stream should deal with that appropriately. [[events]] == EVENTS +=== Assignee Changed + +Sent when the assignee of a change has been modified. + +type:: "assignee-changed" + +change:: link:json.html#change[change attribute] + +changer:: link:json.html#account[account attribute] + +oldAssignee:: Assignee before it was changed. + +eventCreatedOn:: Time in seconds since the UNIX epoch when this event was +created. + === Change Abandoned Sent when a change has been abandoned. diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java new file mode 100644 index 0000000000..022640c288 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java @@ -0,0 +1,29 @@ +// Copyright (C) 2016 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.common.Nullable; +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.extensions.common.AccountInfo; + +/** Notified whenever a change assignee is changed. */ +@ExtensionPoint +public interface AssigneeChangedListener { + interface Event extends ChangeEvent { + @Nullable AccountInfo getOldAssignee(); + } + + void onAssigneeChanged(Event event); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java index c6adef6329..a572c585e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java @@ -30,8 +30,10 @@ import com.google.gerrit.server.account.AccountInfoCacheFactory; import com.google.gerrit.server.account.AccountJson; import com.google.gerrit.server.change.DeleteAssignee.Input; import com.google.gerrit.server.config.AnonymousCowardName; +import com.google.gerrit.server.extensions.events.AssigneeChanged; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.BatchUpdate.ChangeContext; +import com.google.gerrit.server.git.BatchUpdate.Context; import com.google.gerrit.server.git.UpdateException; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gwtorm.server.OrmException; @@ -48,6 +50,7 @@ public class DeleteAssignee implements RestModifyView { private final ChangeMessagesUtil cmUtil; private final Provider db; private final AccountInfoCacheFactory.Factory accountInfos; + private final AssigneeChanged assigneeChanged; private final String anonymousCowardName; @Inject @@ -55,11 +58,13 @@ public class DeleteAssignee implements RestModifyView { ChangeMessagesUtil cmUtil, Provider db, AccountInfoCacheFactory.Factory accountInfosFactory, + AssigneeChanged assigneeChanged, @AnonymousCowardName String anonymousCowardName) { this.batchUpdateFactory = batchUpdateFactory; this.cmUtil = cmUtil; this.db = db; this.accountInfos = accountInfosFactory; + this.assigneeChanged = assigneeChanged; this.anonymousCowardName = anonymousCowardName; } @@ -80,6 +85,7 @@ public class DeleteAssignee implements RestModifyView { } private class Op extends BatchUpdate.Op { + private Change change; private Account deletedAssignee; @Override @@ -88,19 +94,18 @@ public class DeleteAssignee implements RestModifyView { if (!ctx.getControl().canEditAssignee()) { throw new AuthException("Delete Assignee not permitted"); } - Change change = ctx.getChange(); + change = ctx.getChange(); ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); Account.Id currentAssigneeId = change.getAssignee(); if (currentAssigneeId == null) { return false; } - Account account = accountInfos.create().get(currentAssigneeId); + deletedAssignee = accountInfos.create().get(currentAssigneeId); // noteDb update.removeAssignee(); // reviewDb change.setAssignee(null); - addMessage(ctx, update, account); - deletedAssignee = account; + addMessage(ctx, update, deletedAssignee); return true; } @@ -120,5 +125,10 @@ public class DeleteAssignee implements RestModifyView { "Assignee deleted: " + deleted.getName(anonymousCowardName)); cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); } + + @Override + public void postUpdate(Context ctx) throws OrmException { + assigneeChanged.fire(change, ctx.getAccount(), deletedAssignee, ctx.getWhen()); + } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java index 30fd54d459..0262ecb7bf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java @@ -30,7 +30,9 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountInfoCacheFactory; import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.config.AnonymousCowardName; +import com.google.gerrit.server.extensions.events.AssigneeChanged; import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.BatchUpdate.Context; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.validators.AssigneeValidationListener; import com.google.gerrit.server.validators.ValidationException; @@ -49,20 +51,25 @@ public class SetAssigneeOp extends BatchUpdate.Op { private final DynamicSet validationListeners; private final AssigneeInput input; private final String anonymousCowardName; + private final AssigneeChanged assigneeChanged; + private Change change; private Account newAssignee; + private Account oldAssignee; @AssistedInject SetAssigneeOp(AccountsCollection accounts, ChangeMessagesUtil cmUtil, AccountInfoCacheFactory.Factory accountInfosFactory, DynamicSet validationListeners, + AssigneeChanged assigneeChanged, @AnonymousCowardName String anonymousCowardName, @Assisted AssigneeInput input) { this.accounts = accounts; this.cmUtil = cmUtil; this.accountInfosFactory = accountInfosFactory; this.validationListeners = validationListeners; + this.assigneeChanged = assigneeChanged; this.anonymousCowardName = anonymousCowardName; this.input = input; } @@ -73,17 +80,17 @@ public class SetAssigneeOp extends BatchUpdate.Op { if (!ctx.getControl().canEditAssignee()) { throw new AuthException("Changing Assignee not permitted"); } - Change change = ctx.getChange(); + change = ctx.getChange(); ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); Optional oldAssigneeId = - Optional.fromNullable(ctx.getChange().getAssignee()); + Optional.fromNullable(change.getAssignee()); if (input.assignee == null) { if (oldAssigneeId.isPresent()) { throw new BadRequestException("Cannot set Assignee to empty"); } return false; } - Account oldAssignee = null; + oldAssignee = null; if (oldAssigneeId.isPresent()) { oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get()); } @@ -100,7 +107,7 @@ public class SetAssigneeOp extends BatchUpdate.Op { if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) { throw new AuthException(String.format( "Change %s is not visible to %s.", - ctx.getChange().getChangeId(), + change.getChangeId(), newAssigneeUser.getUserName())); } try { @@ -134,14 +141,19 @@ public class SetAssigneeOp extends BatchUpdate.Op { } ChangeMessage cmsg = new ChangeMessage( new ChangeMessage.Key( - ctx.getChange().getId(), + change.getId(), ChangeUtil.messageUUID(ctx.getDb())), ctx.getAccountId(), ctx.getWhen(), - ctx.getChange().currentPatchSetId()); + change.currentPatchSetId()); cmsg.setMessage(msg.toString()); cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); } + @Override + public void postUpdate(Context ctx) throws OrmException { + assigneeChanged.fire(change, ctx.getAccount(), oldAssignee, ctx.getWhen()); + } + public Account getNewAssignee() { return newAssignee; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index 8a13eca845..fecb156706 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -30,6 +30,7 @@ import com.google.gerrit.extensions.config.DownloadScheme; import com.google.gerrit.extensions.config.ExternalIncludedIn; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.events.AgreementSignupListener; +import com.google.gerrit.extensions.events.AssigneeChangedListener; import com.google.gerrit.extensions.events.ChangeAbandonedListener; import com.google.gerrit.extensions.events.ChangeIndexedListener; import com.google.gerrit.extensions.events.ChangeMergedListener; @@ -306,6 +307,7 @@ public class GerritGlobalModule extends FactoryModule { DynamicSet.setOf(binder(), CacheRemovalListener.class); DynamicMap.mapOf(binder(), CapabilityDefinition.class); DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class); + DynamicSet.setOf(binder(), AssigneeChangedListener.class); DynamicSet.setOf(binder(), ChangeAbandonedListener.class); DynamicSet.setOf(binder(), CommentAddedListener.class); DynamicSet.setOf(binder(), DraftPublishedListener.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java index 8c1851463f..8f6035ab53 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java @@ -26,6 +26,7 @@ public class ChangeAttribute { public String number; public String subject; public AccountAttribute owner; + public AccountAttribute assignee; public String url; public String commitMessage; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/AssigneeChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/AssigneeChangedEvent.java new file mode 100644 index 0000000000..60a0935008 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/AssigneeChangedEvent.java @@ -0,0 +1,29 @@ +// Copyright (C) 2016 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.events; + +import com.google.common.base.Supplier; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.data.AccountAttribute; + +public class AssigneeChangedEvent extends ChangeEvent { + static final String TYPE = "assignee-changed"; + public Supplier changer; + public Supplier oldAssignee; + + public AssigneeChangedEvent(Change change) { + super(TYPE, change); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java index a1440e8824..fbeb8359ac 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java @@ -158,6 +158,7 @@ public class EventFactory { } a.url = getChangeUrl(change); a.owner = asAccountAttribute(change.getOwner()); + a.assignee = asAccountAttribute(change.getAssignee()); a.status = change.getStatus(); return a; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java index 447e8b214f..cd6e2f98b7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java @@ -22,6 +22,7 @@ public class EventTypes { private static final Map> typesByString = new HashMap<>(); static { + register(AssigneeChangedEvent.TYPE, AssigneeChangedEvent.class); register(ChangeAbandonedEvent.TYPE, ChangeAbandonedEvent.class); register(ChangeMergedEvent.TYPE, ChangeMergedEvent.class); register(ChangeRestoredEvent.TYPE, ChangeRestoredEvent.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java index 8d093f0ba9..85098d4d5e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java @@ -24,6 +24,7 @@ import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ApprovalInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.RevisionInfo; +import com.google.gerrit.extensions.events.AssigneeChangedListener; import com.google.gerrit.extensions.events.ChangeAbandonedListener; import com.google.gerrit.extensions.events.ChangeMergedListener; import com.google.gerrit.extensions.events.ChangeRestoredListener; @@ -73,6 +74,7 @@ import java.util.Map.Entry; @Singleton public class StreamEventsApiListener implements + AssigneeChangedListener, ChangeAbandonedListener, ChangeMergedListener, ChangeRestoredListener, @@ -91,6 +93,8 @@ public class StreamEventsApiListener implements public static class Module extends LifecycleModule { @Override protected void configure() { + DynamicSet.bind(binder(), AssigneeChangedListener.class) + .to(StreamEventsApiListener.class); DynamicSet.bind(binder(), ChangeAbandonedListener.class) .to(StreamEventsApiListener.class); DynamicSet.bind(binder(), ChangeMergedListener.class) @@ -177,8 +181,8 @@ public class StreamEventsApiListener implements new Supplier() { @Override public AccountAttribute get() { - return eventFactory.asAccountAttribute( - new Account.Id(account._accountId)); + return account != null ? eventFactory.asAccountAttribute( + new Account.Id(account._accountId)) : null; } }); } @@ -265,6 +269,22 @@ public class StreamEventsApiListener implements return null; } + @Override + public void onAssigneeChanged(AssigneeChangedListener.Event ev) { + try { + Change change = getChange(ev.getChange()); + AssigneeChangedEvent event = new AssigneeChangedEvent(change); + + event.change = changeAttributeSupplier(change); + event.changer = accountAttributeSupplier(ev.getWho()); + event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee()); + + dispatcher.get().postEvent(change, event); + } catch (OrmException e) { + log.error("Failed to dispatch event", e); + } + } + @Override public void onTopicEdited(TopicEditedListener.Event ev) { try { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java new file mode 100644 index 0000000000..2234556870 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java @@ -0,0 +1,91 @@ +// Copyright (C) 2016 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.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.AssigneeChangedListener; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Timestamp; + +public class AssigneeChanged { + private static final Logger log = + LoggerFactory.getLogger(AssigneeChanged.class); + + private final DynamicSet listeners; + private final EventUtil util; + + @Inject + AssigneeChanged(DynamicSet listeners, + EventUtil util) { + this.listeners = listeners; + this.util = util; + } + + public void fire(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee, + Timestamp when) { + if (!listeners.iterator().hasNext()) { + return; + } + Event event = new Event(change, editor, oldAssignee, when); + for (AssigneeChangedListener l : listeners) { + try { + l.onAssigneeChanged(event); + } catch (Exception e) { + log.warn("Error in event listener", e); + } + } + } + + public void fire(Change change, Account account, Account oldAssignee, + Timestamp when) { + if (!listeners.iterator().hasNext()) { + return; + } + try { + fire(util.changeInfo(change), + util.accountInfo(account), + util.accountInfo(oldAssignee), + when); + } catch (OrmException e) { + log.error("Couldn't fire event", e); + } + } + + private static class Event extends AbstractChangeEvent + implements AssigneeChangedListener.Event { + private final AccountInfo oldAssignee; + + Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee, + Timestamp when) { + super(change, editor, when, NotifyHandling.ALL); + this.oldAssignee = oldAssignee; + } + + @Override + public AccountInfo getOldAssignee() { + return oldAssignee; + } + } +}