Issue #2751 implemented room/topic ownership transfer

Former-commit-id: cf5041f2d052c4900e2871e0fc8dc1a2b71794de
This commit is contained in:
Brian Clements 2014-02-18 09:49:40 -06:00
parent c13942140a
commit c3cadff705
7 changed files with 346 additions and 52 deletions

View file

@ -33,6 +33,7 @@ import com.raytheon.uf.viz.collaboration.comm.provider.user.VenueParticipant;
* Date Ticket# Engineer Description * Date Ticket# Engineer Description
* ------------ ---------- ----------- -------------------------- * ------------ ---------- ----------- --------------------------
* Feb 11, 2014 njensen Initial creation * Feb 11, 2014 njensen Initial creation
* Feb 19, 2014 2751 bclement added oldLeader field
* *
* </pre> * </pre>
* *
@ -46,12 +47,17 @@ public class LeaderChangeEvent {
@DynamicSerializeElement @DynamicSerializeElement
private VenueParticipant newLeader; private VenueParticipant newLeader;
@DynamicSerializeElement
private VenueParticipant oldLeader;
public LeaderChangeEvent() { public LeaderChangeEvent() {
} }
public LeaderChangeEvent(VenueParticipant newLeader) { public LeaderChangeEvent(VenueParticipant newLeader,
VenueParticipant oldLeader) {
this.newLeader = newLeader; this.newLeader = newLeader;
this.oldLeader = oldLeader;
} }
public VenueParticipant getNewLeader() { public VenueParticipant getNewLeader() {
@ -62,4 +68,19 @@ public class LeaderChangeEvent {
this.newLeader = newLeader; this.newLeader = newLeader;
} }
/**
* @return the oldLeader
*/
public VenueParticipant getOldLeader() {
return oldLeader;
}
/**
* @param oldLeader
* the oldLeader to set
*/
public void setOldLeader(VenueParticipant oldLeader) {
this.oldLeader = oldLeader;
}
} }

View file

@ -0,0 +1,92 @@
/**
* This software was developed and / or modified by Raytheon Company,
* pursuant to Contract DG133W-05-CQ-1067 with the US Government.
*
* U.S. EXPORT CONTROLLED TECHNICAL DATA
* This software product contains export-restricted data whose
* export/transfer/disclosure is restricted by U.S. law. Dissemination
* to non-U.S. persons whether in the United States or abroad requires
* an export license or other authorization.
*
* Contractor Name: Raytheon Company
* Contractor Address: 6825 Pine Street, Suite 340
* Mail Stop B8
* Omaha, NE 68106
* 402.291.0100
*
* See the AWIPS II Master Rights File ("Master Rights File.pdf") for
* further licensing information.
**/
package com.raytheon.uf.viz.collaboration.comm.provider.session;
import org.jivesoftware.smackx.pubsub.Affiliation;
import org.jivesoftware.smackx.pubsub.Node;
import org.jivesoftware.smackx.pubsub.NodeExtension;
import org.jivesoftware.smackx.pubsub.PubSubElementType;
/**
* Packet extension for changing a user's affiliation with a pubsub topic.
* Follows specification at
* http://xmpp.org/extensions/xep-0060.html#owner-affiliations-modify
*
* <pre>
*
* SOFTWARE HISTORY
*
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Feb 18, 2014 2751 bclement Initial creation
*
* </pre>
*
* @author bclement
* @version 1.0
*/
public class ChangeAffiliationExtension extends NodeExtension {
private static final String affiliationName = "affiliation";
private static final String jidAttribute = "jid";
private final String id;
private final Affiliation.Type type;
/**
* @param n
* topic node
* @param id
* userid
* @param type
* new affiliation
*/
public ChangeAffiliationExtension(Node n, String id, Affiliation.Type type) {
super(PubSubElementType.AFFILIATIONS, n.getId());
this.id = id;
this.type = type;
}
/*
* (non-Javadoc)
*
* @see org.jivesoftware.smackx.pubsub.NodeExtension#toXML()
*/
@Override
public String toXML() {
StringBuilder builder = new StringBuilder("<");
builder.append(getElementName());
builder.append(" node='");
builder.append(getNode());
builder.append("'><").append(affiliationName).append(" ");
builder.append(jidAttribute).append("='");
builder.append(this.id);
builder.append("' ").append(affiliationName).append("='");
builder.append(this.type.toString());
builder.append("'/></");
builder.append(getElementName());
builder.append(">");
return builder.toString();
}
}

View file

@ -0,0 +1,68 @@
/**
* This software was developed and / or modified by Raytheon Company,
* pursuant to Contract DG133W-05-CQ-1067 with the US Government.
*
* U.S. EXPORT CONTROLLED TECHNICAL DATA
* This software product contains export-restricted data whose
* export/transfer/disclosure is restricted by U.S. law. Dissemination
* to non-U.S. persons whether in the United States or abroad requires
* an export license or other authorization.
*
* Contractor Name: Raytheon Company
* Contractor Address: 6825 Pine Street, Suite 340
* Mail Stop B8
* Omaha, NE 68106
* 402.291.0100
*
* See the AWIPS II Master Rights File ("Master Rights File.pdf") for
* further licensing information.
**/
package com.raytheon.uf.viz.collaboration.comm.provider.session;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
/**
* PubSub operations not implemented by the Smack PubSub manager. This includes
* operations to modify ownership of a topic node.
*
* <pre>
*
* SOFTWARE HISTORY
*
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Feb 18, 2014 2751 bclement Initial creation
*
* </pre>
*
* @author bclement
* @version 1.0
*/
public class PubSubOperations {
private PubSubOperations() {
}
/**
* Send packet to change affiliation of user on a pubsub topic node
*
* @param conn
* @param affiliation
* @throws XMPPException
*/
public static void sendAffiliationPacket(XMPPConnection conn,
ChangeAffiliationExtension affiliation) throws XMPPException {
PubSub packet = new PubSub();
packet.setType(IQ.Type.SET);
packet.setTo("pubsub." + conn.getServiceName());
packet.setPubSubNamespace(PubSubNamespace.OWNER);
packet.addExtension(affiliation);
SyncPacketSend.getReply(conn, packet);
}
}

View file

@ -79,6 +79,7 @@ import com.raytheon.uf.viz.collaboration.comm.provider.user.VenueParticipant;
* Feb 12, 2014 2793 bclement added additional null check to sendObjectToVenue * Feb 12, 2014 2793 bclement added additional null check to sendObjectToVenue
* Feb 13, 2014 2751 bclement VenueParticipant refactor * Feb 13, 2014 2751 bclement VenueParticipant refactor
* Feb 13, 2014 2751 njensen Added changeLeader() * Feb 13, 2014 2751 njensen Added changeLeader()
* Feb 18, 2014 2751 bclement implemented room and pubsub ownership transfer
* Feb 19, 2014 2751 bclement added isClosed() * Feb 19, 2014 2751 bclement added isClosed()
* *
* </pre> * </pre>
@ -173,7 +174,7 @@ public class SharedDisplaySession extends VenueSession implements
* #sendObjectToVenue(java.lang.Object) * #sendObjectToVenue(java.lang.Object)
*/ */
@Override @Override
public void sendObjectToVenue(Object obj) throws CollaborationException { public void sendObjectToVenue(Object obj){
if (obj == null) { if (obj == null) {
return; return;
} }
@ -196,8 +197,7 @@ public class SharedDisplaySession extends VenueSession implements
* java.lang.Object) * java.lang.Object)
*/ */
@Override @Override
public void sendObjectToPeer(VenueParticipant participant, Object obj) public void sendObjectToPeer(VenueParticipant participant, Object obj) {
throws CollaborationException {
// TODO should only send to CAVE clients // TODO should only send to CAVE clients
if (obj == null) { if (obj == null) {
return; return;
@ -412,10 +412,24 @@ public class SharedDisplaySession extends VenueSession implements
if (payload instanceof SessionPayload) { if (payload instanceof SessionPayload) {
SessionPayload sp = (SessionPayload) payload; SessionPayload sp = (SessionPayload) payload;
Object obj = sp.getData(); Object obj = sp.getData();
if (obj instanceof LeaderChangeEvent) {
handleLeaderChange((LeaderChangeEvent) obj);
}
postEvent(obj); postEvent(obj);
} }
} }
/**
* Apply leadership change event to session
*
* @param event
*/
private void handleLeaderChange(LeaderChangeEvent event) {
VenueParticipant newLeader = event.getNewLeader();
setCurrentDataProvider(newLeader);
setCurrentSessionLeader(newLeader);
}
/** /**
* @return true if session data topic exists * @return true if session data topic exists
*/ */
@ -491,52 +505,113 @@ public class SharedDisplaySession extends VenueSession implements
// we don't persist data packets to topics // we don't persist data packets to topics
} }
/**
* Make user with id an owner of the pubsub topic
*
* @param id
* @throws XMPPException
*/
private void grantTopicOwnership(String id) throws XMPPException {
sendAffiliationPacket(id, Affiliation.Type.owner);
}
/**
* Make user with id a publisher of the pubsub topic
*
* @param id
* @throws XMPPException
*/
private void revokeTopicOwnership(String id) throws XMPPException {
sendAffiliationPacket(id, Affiliation.Type.publisher);
}
/**
* Change affiliation of user with id
*
* @param id
* @param type
* @throws XMPPException
*/
private void sendAffiliationPacket(String id, Affiliation.Type type)
throws XMPPException {
ChangeAffiliationExtension affiliation = new ChangeAffiliationExtension(
topic, id, type);
PubSubOperations.sendAffiliationPacket(conn, affiliation);
}
@Override @Override
public void changeLeader(VenueParticipant newLeader) public void changeLeader(VenueParticipant newLeader)
throws CollaborationException { throws CollaborationException {
if (!getCurrentDataProvider().isSameUser(getUserID())) { final VenueParticipant oldLeader = getCurrentDataProvider();
if (!oldLeader.isSameUser(getUserID())) {
throw new CollaborationException( throw new CollaborationException(
"Only the leader can transfer leadership"); "Only the leader can transfer leadership");
} }
if (!newLeader.hasActualUserId()) {
throw new CollaborationException(
"Unable to grant ownership because new leader's actual userid is not known");
}
boolean ownershipGranted = false; final String newLeaderId = newLeader.getUserid().getNormalizedId();
boolean topicOwnershipGranted = false;
boolean roomOwnershipGranted = false;
boolean othersNotified = false; boolean othersNotified = false;
String revokeTarget = null;
try { try {
// was formerly the data provider, so hand off pubsub ownership // was formerly the data provider, so hand off pubsub ownership
// TODO change pubsub ownership grantTopicOwnership(newLeaderId);
topicOwnershipGranted = true;
// change the room's ownership cause the leader needs to know // change the room's ownership cause the leader needs to know
// participants' jids to properly complete some leader actions // participants' jids to properly complete some leader actions
// FIXME muc.grantOwnership(newLeaderId);
// muc.grantOwnership(newLeader.getFQName()); roomOwnershipGranted = true;
ownershipGranted = true;
// TODO if we get private chat within a chat room working, we // TODO if we get private chat within a chat room working, we
// shouldn't need to let everyone know the leader's jid // shouldn't need to let everyone know the leader's jid
LeaderChangeEvent event = new LeaderChangeEvent(newLeader); LeaderChangeEvent event = new LeaderChangeEvent(newLeader,
oldLeader);
this.sendObjectToVenue(event); this.sendObjectToVenue(event);
othersNotified = true; othersNotified = true;
// TODO revoke pubsub ownership
// revoke our own ownership, last action in case other parts // revoke our own ownership, last action in case other parts
// of the transfer fail // of the transfer fail
// FIXME revokeTarget = "topic";
// muc.revokeOwnership(getUserID().getFQName()); UserId account = getAccount();
} catch (Exception e) { revokeTopicOwnership(account.getNormalizedId());
// TODO change to catch XMPPConnection, ensure this is transactional // we revoke admin instead of ownership because it sets back to
if (ownershipGranted && !othersNotified) { // 'member' instead of just down to 'admin'
// transaction, attempt to roll back the ownership change revokeTarget = "room";
muc.revokeAdmin(account.getNormalizedId());
} catch (XMPPException e) {
if (!othersNotified) {
// transaction, attempt to roll back the ownership changes
// because other participants didn't hear about it // because other participants didn't hear about it
if (roomOwnershipGranted) {
try { try {
muc.revokeAdmin(newLeader.getFQName()); muc.revokeAdmin(newLeaderId);
} catch (XMPPException e1) { } catch (XMPPException e1) {
// TODO we only want to throw up the original error, not log.error(
// the rollback error, is printStackTrace good enough? "Problem rolling back room ownership transfer",
e1.printStackTrace(); e1);
} }
} }
throw new CollaborationException("Error transferring leadership", e); if (topicOwnershipGranted) {
try{
revokeTopicOwnership(newLeaderId);
} catch (XMPPException e1) {
log.error(
"Problem rolling back topic ownership transfer",
e1);
}
}
throw new CollaborationException(
"Error transferring leadership", e);
} else {
log.warn("Problem releasing ownership of " + revokeTarget
+ ". " + e.getLocalizedMessage());
}
} }
} }

View file

@ -41,6 +41,8 @@ import org.jivesoftware.smackx.muc.UserStatusListener;
import org.jivesoftware.smackx.packet.DiscoverItems; import org.jivesoftware.smackx.packet.DiscoverItems;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.raytheon.uf.common.status.IUFStatusHandler;
import com.raytheon.uf.common.status.UFStatus;
import com.raytheon.uf.viz.collaboration.comm.Activator; import com.raytheon.uf.viz.collaboration.comm.Activator;
import com.raytheon.uf.viz.collaboration.comm.identity.CollaborationException; import com.raytheon.uf.viz.collaboration.comm.identity.CollaborationException;
import com.raytheon.uf.viz.collaboration.comm.identity.IMessage; import com.raytheon.uf.viz.collaboration.comm.identity.IMessage;
@ -95,6 +97,7 @@ import com.raytheon.uf.viz.collaboration.comm.provider.user.VenueParticipant;
* Jan 30, 2014 2698 bclement changed UserId to VenueParticipant, added handle * Jan 30, 2014 2698 bclement changed UserId to VenueParticipant, added handle
* Feb 13, 2014 2751 bclement VenueParticipant refactor * Feb 13, 2014 2751 bclement VenueParticipant refactor
* Feb 18, 2014 2751 bclement Fixed history message 'from' type * Feb 18, 2014 2751 bclement Fixed history message 'from' type
* Feb 18, 2014 2751 bclement log privilege changes instead of spamming chat window
* *
* </pre> * </pre>
* *
@ -107,6 +110,9 @@ import com.raytheon.uf.viz.collaboration.comm.provider.user.VenueParticipant;
public class VenueSession extends BaseSession implements IVenueSession { public class VenueSession extends BaseSession implements IVenueSession {
private static final IUFStatusHandler log = UFStatus
.getHandler(VenueSession.class);
private static final String SEND_TXT = "[[TEXT]]"; private static final String SEND_TXT = "[[TEXT]]";
public static final String SEND_HISTORY = "[[HISTORY]]"; public static final String SEND_HISTORY = "[[HISTORY]]";
@ -431,13 +437,13 @@ public class VenueSession extends BaseSession implements IVenueSession {
@Override @Override
public void ownershipRevoked(String participant) { public void ownershipRevoked(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is no longer a room owner."); "is no longer a room owner.");
} }
@Override @Override
public void ownershipGranted(String participant) { public void ownershipGranted(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is now a room owner."); "is now a room owner.");
} }
@ -450,25 +456,25 @@ public class VenueSession extends BaseSession implements IVenueSession {
@Override @Override
public void moderatorRevoked(String participant) { public void moderatorRevoked(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is no longer a moderator."); "is no longer a moderator.");
} }
@Override @Override
public void moderatorGranted(String participant) { public void moderatorGranted(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is now a moderator."); "is now a moderator.");
} }
@Override @Override
public void membershipRevoked(String participant) { public void membershipRevoked(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is no longer a member of the room."); "is no longer a member of the room.");
} }
@Override @Override
public void membershipGranted(String participant) { public void membershipGranted(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is now a member of the room."); "is now a member of the room.");
} }
@ -502,16 +508,26 @@ public class VenueSession extends BaseSession implements IVenueSession {
@Override @Override
public void adminRevoked(String participant) { public void adminRevoked(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is no longer an admin."); "is no longer an admin.");
} }
@Override @Override
public void adminGranted(String participant) { public void adminGranted(String participant) {
sendParticipantEvent(participant, ParticipantEventType.UPDATED, logParticipantEvent(participant, ParticipantEventType.UPDATED,
"is now an admin."); "is now an admin.");
} }
private void logParticipantEvent(String participant,
ParticipantEventType type, String desciption) {
StringBuilder builder = new StringBuilder();
IVenue v = getVenue();
builder.append("In session '").append(v.getName())
.append("': ");
builder.append(participant).append(" ").append(desciption);
log.debug(builder.toString());
}
private void sendParticipantEvent(String participant, private void sendParticipantEvent(String participant,
ParticipantEventType type, String desciption) { ParticipantEventType type, String desciption) {
VenueParticipant user = IDConverter.convertFromRoom(muc, VenueParticipant user = IDConverter.convertFromRoom(muc,
@ -577,32 +593,32 @@ public class VenueSession extends BaseSession implements IVenueSession {
@Override @Override
public void ownershipRevoked() { public void ownershipRevoked() {
sendUserEvent("You are no longer an owner of this room."); logUserEvent("You are no longer an owner of this room.");
} }
@Override @Override
public void ownershipGranted() { public void ownershipGranted() {
sendUserEvent("You are now an owner of this room."); logUserEvent("You are now an owner of this room.");
} }
@Override @Override
public void moderatorRevoked() { public void moderatorRevoked() {
sendUserEvent("You are no longer a moderator of this room."); logUserEvent("You are no longer a moderator of this room.");
} }
@Override @Override
public void moderatorGranted() { public void moderatorGranted() {
sendUserEvent("You are now the moderator of this room."); logUserEvent("You are now the moderator of this room.");
} }
@Override @Override
public void membershipRevoked() { public void membershipRevoked() {
sendUserEvent("You are no longer a member of this room."); logUserEvent("You are no longer a member of this room.");
} }
@Override @Override
public void membershipGranted() { public void membershipGranted() {
sendUserEvent("You are now a member of this room."); logUserEvent("You are now a member of this room.");
} }
@Override @Override
@ -623,17 +639,27 @@ public class VenueSession extends BaseSession implements IVenueSession {
@Override @Override
public void adminRevoked() { public void adminRevoked() {
sendUserEvent("You have had admin privileges revoked."); logUserEvent("You have had admin privileges revoked.");
} }
@Override @Override
public void adminGranted() { public void adminGranted() {
sendUserEvent("You have had admin privileges granted."); logUserEvent("You have had admin privileges granted.");
} }
private void sendUserEvent(String message) { private void sendUserEvent(String message) {
postEvent(new VenueUserEvent(message)); postEvent(new VenueUserEvent(message));
} }
private void logUserEvent(String message) {
StringBuilder builder = new StringBuilder();
IVenue v = getVenue();
builder.append("In session '").append(v.getName())
.append("': ");
builder.append(message);
log.info(builder.toString());
}
}); });
} }

View file

@ -135,9 +135,7 @@ public class SessionContainer {
// for now (and possibly forever) we are not allowing capabilities to be // for now (and possibly forever) we are not allowing capabilities to be
// transferred separately // transferred separately
VenueParticipant newLeader = event.getNewLeader(); VenueParticipant newLeader = event.getNewLeader();
VenueParticipant oldLeader = session.getCurrentDataProvider(); VenueParticipant oldLeader = event.getOldLeader();
session.setCurrentDataProvider(newLeader);
session.setCurrentSessionLeader(newLeader);
if (session.getUserID().isSameUser(oldLeader)) { if (session.getUserID().isSameUser(oldLeader)) {
// just gave up our leadership // just gave up our leadership

View file

@ -54,6 +54,7 @@ import com.raytheon.uf.viz.collaboration.comm.identity.ISharedDisplaySession;
import com.raytheon.uf.viz.collaboration.comm.identity.IVenueSession; import com.raytheon.uf.viz.collaboration.comm.identity.IVenueSession;
import com.raytheon.uf.viz.collaboration.comm.identity.invite.ColorPopulator; import com.raytheon.uf.viz.collaboration.comm.identity.invite.ColorPopulator;
import com.raytheon.uf.viz.collaboration.comm.identity.user.SharedDisplayRole; import com.raytheon.uf.viz.collaboration.comm.identity.user.SharedDisplayRole;
import com.raytheon.uf.viz.collaboration.comm.provider.event.LeaderChangeEvent;
import com.raytheon.uf.viz.collaboration.comm.provider.user.VenueParticipant; import com.raytheon.uf.viz.collaboration.comm.provider.user.VenueParticipant;
import com.raytheon.uf.viz.collaboration.display.IRemoteDisplayContainer; import com.raytheon.uf.viz.collaboration.display.IRemoteDisplayContainer;
import com.raytheon.uf.viz.collaboration.display.IRemoteDisplayContainer.IRemoteDisplayChangedListener; import com.raytheon.uf.viz.collaboration.display.IRemoteDisplayContainer.IRemoteDisplayChangedListener;
@ -94,6 +95,7 @@ import com.raytheon.viz.ui.input.EditableManager;
* Jan 28, 2014 2698 bclement removed venue info * Jan 28, 2014 2698 bclement removed venue info
* Jan 30, 2014 2698 bclement changed UserId to VenueParticipant * Jan 30, 2014 2698 bclement changed UserId to VenueParticipant
* Feb 12, 2014 2751 njensen Added transfer leadership and shutdown safety * Feb 12, 2014 2751 njensen Added transfer leadership and shutdown safety
* Feb 18, 2014 2751 bclement update participants list and notify on leader change
* *
* </pre> * </pre>
* *
@ -835,11 +837,23 @@ public class CollaborationSessionView extends SessionView implements
if (newDisplayContainer != null) { if (newDisplayContainer != null) {
newDisplayContainer.addRemoteDisplayChangedListener(this); newDisplayContainer.addRemoteDisplayChangedListener(this);
} }
// this really only happens when the leader changes, so refresh
if (this.usersTable != null && !usersTable.getTable().isDisposed()) {
this.usersTable.refresh();
} }
@Subscribe
public void leaderChanged(final LeaderChangeEvent event) {
VizApp.runAsync(new Runnable() {
@Override
public void run() {
if (usersTable != null
&& !usersTable.getTable().isDisposed()) {
usersTable.setInput(session.getVenue().getParticipants());
usersTable.refresh();
}
sendParticipantSystemMessage(event.getNewLeader(),
" is now leader.");
updateToolItems(); updateToolItems();
} }
});
}
} }