Private state changed event

Some ticket handling and integration systems will need to know when
the private state has changed on a change. If a change is uploaded
as a private change it would in many instances not warrant any action
from said systems, but once the change is umarked as private it would,
so there needs to be an event that advertises that state change.

Bug: Issue 7360
Change-Id: I59202222e9860900c2fa42360705396ee828c280
This commit is contained in:
Sven Selberg
2017-12-11 14:17:44 +01:00
parent bb521dcf85
commit 05354eca9c
12 changed files with 191 additions and 8 deletions

View File

@@ -274,6 +274,20 @@ changer:: link:json.html#account[account attribute]
eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
created.
=== Private State Changed
Sent when the the link:intro-user.html#private-changes[private] state of the
change has changed.
type:: private-state-changed
change:: link:json.html#change[change attribute]
changer:: link:json.html#account[account attribute]
eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
created.
=== Vote Deleted
Sent when a vote was removed from a change.

View File

@@ -0,0 +1,21 @@
// Copyright (C) 2017 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;
public interface PrivateStateChangedListener {
interface Event extends ChangeEvent {}
void onPrivateStateChanged(Event event);
}

View File

@@ -40,17 +40,20 @@ public class DeletePrivate
private final ChangeMessagesUtil cmUtil;
private final Provider<ReviewDb> dbProvider;
private final PermissionBackend permissionBackend;
private final SetPrivateOp.Factory setPrivateOpFactory;
@Inject
DeletePrivate(
Provider<ReviewDb> dbProvider,
RetryHelper retryHelper,
ChangeMessagesUtil cmUtil,
PermissionBackend permissionBackend) {
PermissionBackend permissionBackend,
SetPrivateOp.Factory setPrivateOpFactory) {
super(retryHelper);
this.dbProvider = dbProvider;
this.cmUtil = cmUtil;
this.permissionBackend = permissionBackend;
this.setPrivateOpFactory = setPrivateOpFactory;
}
@Override
@@ -65,7 +68,7 @@ public class DeletePrivate
throw new ResourceConflictException("change is not private");
}
SetPrivateOp op = new SetPrivateOp(cmUtil, false, input);
SetPrivateOp op = setPrivateOpFactory.create(cmUtil, false, input);
try (BatchUpdate u =
updateFactory.create(
dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {

View File

@@ -32,8 +32,9 @@ public class DeletePrivateByPost extends DeletePrivate implements UiAction<Chang
Provider<ReviewDb> dbProvider,
RetryHelper retryHelper,
ChangeMessagesUtil cmUtil,
PermissionBackend permissionBackend) {
super(dbProvider, retryHelper, cmUtil, permissionBackend);
PermissionBackend permissionBackend,
SetPrivateOp.Factory setPrivateOpFactory) {
super(dbProvider, retryHelper, cmUtil, permissionBackend, setPrivateOpFactory);
}
@Override

View File

@@ -177,6 +177,7 @@ public class Module extends RestApiModule {
factory(ReviewerResource.Factory.class);
factory(SetAssigneeOp.Factory.class);
factory(SetHashtagsOp.Factory.class);
factory(SetPrivateOp.Factory.class);
factory(WorkInProgressOp.Factory.class);
}
}

View File

@@ -43,17 +43,20 @@ public class PostPrivate
private final ChangeMessagesUtil cmUtil;
private final Provider<ReviewDb> dbProvider;
private final PermissionBackend permissionBackend;
private final SetPrivateOp.Factory setPrivateOpFactory;
@Inject
PostPrivate(
Provider<ReviewDb> dbProvider,
RetryHelper retryHelper,
ChangeMessagesUtil cmUtil,
PermissionBackend permissionBackend) {
PermissionBackend permissionBackend,
SetPrivateOp.Factory setPrivateOpFactory) {
super(retryHelper);
this.dbProvider = dbProvider;
this.cmUtil = cmUtil;
this.permissionBackend = permissionBackend;
this.setPrivateOpFactory = setPrivateOpFactory;
}
@Override
@@ -68,7 +71,7 @@ public class PostPrivate
return Response.ok("");
}
SetPrivateOp op = new SetPrivateOp(cmUtil, true, input);
SetPrivateOp op = setPrivateOpFactory.create(cmUtil, true, input);
try (BatchUpdate u =
updateFactory.create(
dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {

View File

@@ -19,10 +19,14 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.extensions.events.PrivateStateChanged;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.Context;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
public class SetPrivateOp implements BatchUpdateOp {
public static class Input {
@@ -35,19 +39,32 @@ public class SetPrivateOp implements BatchUpdateOp {
}
}
public interface Factory {
SetPrivateOp create(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input);
}
private final ChangeMessagesUtil cmUtil;
private final boolean isPrivate;
private final Input input;
private final PrivateStateChanged privateStateChanged;
SetPrivateOp(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input) {
private Change change;
@Inject
SetPrivateOp(
PrivateStateChanged privateStateChanged,
@Assisted ChangeMessagesUtil cmUtil,
@Assisted boolean isPrivate,
@Assisted Input input) {
this.cmUtil = cmUtil;
this.isPrivate = isPrivate;
this.input = input;
this.privateStateChanged = privateStateChanged;
}
@Override
public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, OrmException {
Change change = ctx.getChange();
change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
change.setPrivate(isPrivate);
change.setLastUpdatedOn(ctx.getWhen());
@@ -56,6 +73,11 @@ public class SetPrivateOp implements BatchUpdateOp {
return true;
}
@Override
public void postUpdate(Context ctx) {
privateStateChanged.fire(change, ctx.getAccount(), ctx.getWhen());
}
private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
Change c = ctx.getChange();
StringBuilder buf = new StringBuilder(c.isPrivate() ? "Set private" : "Unset private");

View File

@@ -49,6 +49,7 @@ import com.google.gerrit.extensions.events.HeadUpdatedListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.events.PluginEventListener;
import com.google.gerrit.extensions.events.PrivateStateChangedListener;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.events.ReviewerAddedListener;
import com.google.gerrit.extensions.events.ReviewerDeletedListener;
@@ -319,6 +320,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), ChangeMergedListener.class);
DynamicSet.setOf(binder(), ChangeRestoredListener.class);
DynamicSet.setOf(binder(), ChangeRevertedListener.class);
DynamicSet.setOf(binder(), PrivateStateChangedListener.class);
DynamicSet.setOf(binder(), ReviewerAddedListener.class);
DynamicSet.setOf(binder(), ReviewerDeletedListener.class);
DynamicSet.setOf(binder(), VoteDeletedListener.class);

View File

@@ -30,6 +30,7 @@ public class EventTypes {
register(CommitReceivedEvent.TYPE, CommitReceivedEvent.class);
register(HashtagsChangedEvent.TYPE, HashtagsChangedEvent.class);
register(PatchSetCreatedEvent.TYPE, PatchSetCreatedEvent.class);
register(PrivateStateChangedEvent.TYPE, PrivateStateChangedEvent.class);
register(ProjectCreatedEvent.TYPE, ProjectCreatedEvent.class);
register(RefReceivedEvent.TYPE, RefReceivedEvent.class);
register(RefUpdatedEvent.TYPE, RefUpdatedEvent.class);

View File

@@ -0,0 +1,28 @@
// Copyright (C) 2017 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 PrivateStateChangedEvent extends ChangeEvent {
static final String TYPE = "private-state-changed";
public Supplier<AccountAttribute> changer;
protected PrivateStateChangedEvent(Change change) {
super(TYPE, change);
}
}

View File

@@ -32,6 +32,7 @@ import com.google.gerrit.extensions.events.CommentAddedListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.HashtagsEditedListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.events.PrivateStateChangedListener;
import com.google.gerrit.extensions.events.ReviewerAddedListener;
import com.google.gerrit.extensions.events.ReviewerDeletedListener;
import com.google.gerrit.extensions.events.RevisionCreatedListener;
@@ -79,6 +80,7 @@ public class StreamEventsApiListener
ChangeMergedListener,
ChangeRestoredListener,
WorkInProgressStateChangedListener,
PrivateStateChangedListener,
CommentAddedListener,
GitReferenceUpdatedListener,
HashtagsEditedListener,
@@ -102,6 +104,8 @@ public class StreamEventsApiListener
.to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), HashtagsEditedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), PrivateStateChangedListener.class)
.to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ReviewerAddedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ReviewerDeletedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), RevisionCreatedListener.class).to(StreamEventsApiListener.class);
@@ -481,6 +485,21 @@ public class StreamEventsApiListener
}
}
@Override
public void onPrivateStateChanged(PrivateStateChangedListener.Event ev) {
try {
Change change = getChange(ev.getChange());
PrivateStateChangedEvent event = new PrivateStateChangedEvent(change);
event.change = changeAttributeSupplier(change);
event.changer = accountAttributeSupplier(ev.getWho());
dispatcher.get().postEvent(change, event);
} catch (OrmException | PermissionBackendException e) {
log.error("Failed to dispatch event", e);
}
}
@Override
public void onVoteDeleted(VoteDeletedListener.Event ev) {
try {

View File

@@ -0,0 +1,68 @@
// Copyright (C) 2017 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.PrivateStateChangedListener;
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 java.sql.Timestamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PrivateStateChanged {
private static final Logger log = LoggerFactory.getLogger(PrivateStateChanged.class);
private final DynamicSet<PrivateStateChangedListener> listeners;
private final EventUtil util;
@Inject
PrivateStateChanged(DynamicSet<PrivateStateChangedListener> listeners, EventUtil util) {
this.listeners = listeners;
this.util = util;
}
public void fire(Change change, Account account, Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
Event event = new Event(util.changeInfo(change), util.accountInfo(account), when);
for (PrivateStateChangedListener l : listeners) {
try {
l.onPrivateStateChanged(event);
} catch (Exception e) {
util.logEventListenerError(event, l, e);
}
}
} catch (OrmException e) {
log.error("Couldn't fire event", e);
}
}
private static class Event extends AbstractChangeEvent
implements PrivateStateChangedListener.Event {
protected Event(ChangeInfo change, AccountInfo who, Timestamp when) {
super(change, who, when, NotifyHandling.ALL);
}
}
}