/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* 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 net.java.sip.communicator.impl.protocol.jabber;
import java.util.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.*;
import org.jivesoftware.smackx.muc.*;
/**
* A jabber implementation of the multi user chat operation set.
*
* @author Emil Ivov
* @author Yana Stamcheva
*/
public class OperationSetMultiUserChatJabberImpl
extends AbstractOperationSetMultiUserChat
implements SubscriptionListener
{
/**
* This class logger.
*/
private static final Logger logger
= Logger.getLogger(OperationSetMultiUserChatJabberImpl.class);
/**
* The currently valid Jabber protocol provider service implementation.
*/
private final ProtocolProviderServiceJabberImpl jabberProvider;
/**
* A list of the rooms that are currently open by this account. Note that
* we have not necessarily joined these rooms, we might have simply been
* searching through them.
*/
private final Hashtable<String, ChatRoom> chatRoomCache
= new Hashtable<String, ChatRoom>();
/**
* The registration listener that would get notified when the underlying
* Jabber provider gets registered.
*/
private final RegistrationStateListener providerRegListener
= new RegistrationStateListener();
/**
* A reference to the persistent presence operation set that we use
* to match incoming messages to <tt>Contact</tt>s and vice versa.
*/
private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null;
/**
* Instantiates the user operation set with a currently valid instance of
* the Jabber protocol provider.
* @param jabberProvider a currently valid instance of
* ProtocolProviderServiceJabberImpl.
*/
OperationSetMultiUserChatJabberImpl(
ProtocolProviderServiceJabberImpl jabberProvider)
{
this.jabberProvider = jabberProvider;
jabberProvider.addRegistrationStateChangeListener(providerRegListener);
opSetPersPresence
= (OperationSetPersistentPresenceJabberImpl)jabberProvider
.getOperationSet(OperationSetPersistentPresence.class);
opSetPersPresence.addSubscriptionListener(this);
}
/**
* Add SmackInvitationRejectionListener to <tt>MultiUserChat</tt> instance
* which will dispatch all rejection events.
*
* @param muc the smack MultiUserChat instance that we're going to wrap our
* chat room around.
* @param chatRoom the associated chat room instance
*/
public void addSmackInvitationRejectionListener(MultiUserChat muc,
ChatRoom chatRoom)
{
muc.addInvitationRejectionListener(
new SmackInvitationRejectionListener(chatRoom));
}
/**
* Creates a room with the named <tt>roomName</tt> and according to the
* specified <tt>roomProperties</tt> on the server that this protocol
* provider is currently connected to.
*
* @param roomName the name of the <tt>ChatRoom</tt> to create.
* @param roomProperties properties specifying how the room should be
* created.
*
* @throws OperationFailedException if the room couldn't be created for
* some reason (e.g. room already exists; user already joined to an
* existent room or user has no permissions to create a chat room).
* @throws OperationNotSupportedException if chat room creation is not
* supported by this server
*
* @return ChatRoom the chat room that we've just created.
*/
public ChatRoom createChatRoom(
String roomName,
Map<String, Object> roomProperties)
throws OperationFailedException,
OperationNotSupportedException
{
//first make sure we are connected and the server supports multichat
assertSupportedAndConnected();
ChatRoom room = null;
if (roomName == null)
roomName = "chatroom-" + StringUtils.randomString(4);
else
room = findRoom(roomName);
if (room == null)
{
if (logger.isInfoEnabled())
logger.info("Find room returns null.");
// rooms using google servers needs a special name.
// in the form private-chat-UUID@groupchat.google.com
if(getXmppConnection().getHost().toLowerCase().contains("google"))
{
roomName = "private-chat-" +
UUID.randomUUID().toString() + "@groupchat.google.com";
}
MultiUserChat muc = null;
try
{
muc = new MultiUserChat(
getXmppConnection(), getCanonicalRoomName(roomName));
muc.create(JabberActivator.getGlobalDisplayDetailsService()
.getDisplayName(jabberProvider));
}
catch (XMPPException ex)
{
logger.error("Failed to create chat room.", ex);
throw new OperationFailedException("Failed to create chat room"
, ex.getXMPPError().getCode()
, ex.getCause());
}
boolean isPrivate = false;
if(roomProperties != null)
{
Object isPrivateObject = roomProperties.get("isPrivate");
if(isPrivateObject != null)
{
isPrivate = isPrivateObject.equals(true);
}
}
try
{
Form form;
if(isPrivate)
{
Form initForm = muc.getConfigurationForm();
form = initForm.createAnswerForm();
Iterator<FormField> fieldIterator = initForm.getFields();
while(fieldIterator.hasNext())
{
FormField initField = fieldIterator.next();
if( initField == null ||
initField.getVariable() == null ||
initField.getType() == FormField.TYPE_FIXED ||
initField.getType() == FormField.TYPE_HIDDEN)
continue;
FormField submitField
= form.getField(initField.getVariable());
if(submitField == null)
continue;
Iterator<String> value = initField.getValues();
while(value.hasNext())
submitField.addValue(value.next());
}
String[] fields = {"muc#roomconfig_membersonly",
"muc#roomconfig_allowinvites",
"muc#roomconfig_publicroom"};
Boolean[] values = {true, true, false};
for(int i = 0; i < fields.length; i++)
{
FormField field = new FormField(fields[i]);
field.setType("boolean");
form.addField(field);
form.setAnswer(fields[i], values[i]);
}
}
else
{
form = new Form(Form.TYPE_SUBMIT);
}
muc.sendConfigurationForm(form);
} catch (XMPPException e)
{
logger.error("Failed to send config form.", e);
}
room = createLocalChatRoomInstance(muc);
// as we are creating the room we are the owner of it
// at least that's what MultiUserChat.create says
room.setLocalUserRole(ChatRoomMemberRole.OWNER);
}
return room;
}
/**
* Creates a <tt>ChatRoom</tt> from the specified smack
* <tt>MultiUserChat</tt>.
*
* @param muc the smack MultiUserChat instance that we're going to wrap our
* chat room around.
*
* @return ChatRoom the chat room that we've just created.
*/
private ChatRoom createLocalChatRoomInstance(MultiUserChat muc)
{
synchronized(chatRoomCache)
{
ChatRoomJabberImpl chatRoom
= new ChatRoomJabberImpl(muc, jabberProvider);
cacheChatRoom(chatRoom);
// Add the contained in this class SmackInvitationRejectionListener
// which will dispatch all rejection events to the
// ChatRoomInvitationRejectionListener.
addSmackInvitationRejectionListener(muc, chatRoom);
return chatRoom;
}
}
/**
* Returns a reference to a chatRoom named <tt>roomName</tt>. If the room
* doesn't exists in the cache it creates it.
*
* @param roomName the name of the <tt>ChatRoom</tt> that we're looking
* for.
* @return the <tt>ChatRoom</tt> named <tt>roomName</tt>
* @throws OperationFailedException if an error occurs while trying to
* discover the room on the server.
* @throws OperationNotSupportedException if the server does not support
* multi user chat
*/
public synchronized ChatRoom findRoom(String roomName)
throws OperationFailedException, OperationNotSupportedException
{
//make sure we are connected and multichat is supported.
assertSupportedAndConnected();
String canonicalRoomName = getCanonicalRoomName(roomName);
ChatRoom room = chatRoomCache.get(canonicalRoomName);
if (room != null)
return room;
MultiUserChat muc
= new MultiUserChat(getXmppConnection(), canonicalRoomName);
room = new ChatRoomJabberImpl(muc, jabberProvider);
chatRoomCache.put(canonicalRoomName, room);
return room;
}
/**
* Returns a list of the chat rooms that we have joined and are currently
* active in.
*
* @return a <tt>List</tt> of the rooms where the user has joined using
* a given connection.
*/
public List<ChatRoom> getCurrentlyJoinedChatRooms()
{
synchronized(chatRoomCache)
{
List<ChatRoom> joinedRooms
= new LinkedList<ChatRoom>(this.chatRoomCache.values());
Iterator<ChatRoom> joinedRoomsIter = joinedRooms.iterator();
while (joinedRoomsIter.hasNext())
if (!joinedRoomsIter.next().isJoined())
joinedRoomsIter.remove();
return joinedRooms;
}
}
// **this method is not used**
//
// /**
// * Returns a list of the names of all chat rooms that <tt>contact</tt> is
// * currently a member of.
// *
// * @param contact the contact whose current ChatRooms we will be
// * querying.
// * @return a list of <tt>String</tt> indicating the names of the chat rooms
// * that <tt>contact</tt> has joined and is currently active in.
// *
// * @throws OperationFailedException if an error occurs while trying to
// * discover the room on the server.
// * @throws OperationNotSupportedException if the server does not support
// * multi user chat
// */
// public List getCurrentlyJoinedChatRooms(Contact contact)
// throws OperationFailedException, OperationNotSupportedException
// {
// assertSupportedAndConnected();
//
// Iterator joinedRoomsIter
// = MultiUserChat.getJoinedRooms( getXmppConnection()
// , contact.getAddress());
//
// List joinedRoomsForContact = new LinkedList();
//
// while ( joinedRoomsIter.hasNext() )
// {
// MultiUserChat muc = (MultiUserChat)joinedRoomsIter.next();
// joinedRoomsForContact.add(muc.getRoom());
// }
//
// return joinedRoomsForContact;
// }
/**
* Returns the <tt>List</tt> of <tt>String</tt>s indicating chat rooms
* currently available on the server that this protocol provider is
* connected to.
*
* @return a <tt>java.util.List</tt> of the name <tt>String</tt>s for chat
* rooms that are currently available on the server that this protocol
* provider is connected to.
*
* @throws OperationFailedException if we faile retrieving this list from
* the server.
* @throws OperationNotSupportedException if the server does not support
* multi user chat
*/
public List<String> getExistingChatRooms()
throws OperationFailedException,
OperationNotSupportedException
{
assertSupportedAndConnected();
List<String> list = new LinkedList<String>();
//first retrieve all conference service names available on this server
Iterator<String> serviceNames = null;
try
{
serviceNames = MultiUserChat
.getServiceNames(getXmppConnection()).iterator();
}
catch (XMPPException ex)
{
throw new OperationFailedException(
"Failed to retrieve Jabber conference service names"
, OperationFailedException.GENERAL_ERROR
, ex);
}
//now retrieve all chat rooms currently available for every service name
while(serviceNames.hasNext())
{
String serviceName = serviceNames.next();
List<HostedRoom> roomsOnThisService = new LinkedList<HostedRoom>();
try
{
roomsOnThisService
.addAll(MultiUserChat.getHostedRooms(getXmppConnection()
, serviceName));
}
catch (XMPPException ex)
{
logger.error("Failed to retrieve rooms for serviceName="
+ serviceName, ex);
//continue bravely with other service names
continue;
}
//now go through all rooms available on this service
Iterator<HostedRoom> serviceRoomsIter = roomsOnThisService.iterator();
//add the room name to the list of names we are returning
while(serviceRoomsIter.hasNext())
list.add(
serviceRoomsIter.next().getJid());
}
/** @todo maybe we should add a check here and fail if retrieving chat
* rooms failed for all service names*/
return list;
}
/**
* Returns true if <tt>contact</tt> supports multi user chat sessions.
*
* @param contact reference to the contact whose support for chat rooms
* we are currently querying.
* @return a boolean indicating whether <tt>contact</tt> supports
* chatrooms.
* @todo Implement this
* net.java.sip.communicator.service.protocol.OperationSetMultiUserChat
* method
*/
public boolean isMultiChatSupportedByContact(Contact contact)
{
if(contact.getProtocolProvider()
.getOperationSet(OperationSetMultiUserChat.class) != null)
return true;
return false;
}
/**
* Checks if the contact address is associated with private messaging
* contact or not.
*
* @return <tt>true</tt> if the contact address is associated with private
* messaging contact and <tt>false</tt> if not.
*/
public boolean isPrivateMessagingContact(String contactAddress)
{
return opSetPersPresence.isPrivateMessagingContact(contactAddress);
}
/**
* Informs the sender of an invitation that we decline their invitation.
*
* @param invitation the connection to use for sending the rejection.
* @param rejectReason the reason to reject the given invitation
*/
public void rejectInvitation(ChatRoomInvitation invitation,
String rejectReason)
{
MultiUserChat.decline(jabberProvider.getConnection(),
invitation.getTargetChatRoom().getName(),
invitation.getInviter(),
rejectReason);
}
/**
* Almost all <tt>MultiUserChat</tt> methods require an xmpp connection
* param so I added this method only for the sake of utility.
*
* @return the XMPP connection currently in use by the jabber provider or
* null if jabber provider has yet to be initialized.
*/
private Connection getXmppConnection()
{
return (jabberProvider == null)
? null
:jabberProvider.getConnection();
}
/**
* Makes sure that we are properly connected and that the server supports
* multi user chats.
*
* @throws OperationFailedException if the provider is not registered or
* the xmpp connection not connected.
* @throws OperationNotSupportedException if the service is not supported
* by the server.
*/
private void assertSupportedAndConnected()
throws OperationFailedException, OperationNotSupportedException
{
//throw an exception if the provider is not registered or the xmpp
//connection not connected.
if( !jabberProvider.isRegistered()
|| !getXmppConnection().isConnected())
{
throw new OperationFailedException(
"Provider not connected to jabber server"
, OperationFailedException.NETWORK_FAILURE);
}
//MultiUserChat.isServiceEnabled() *always* returns false,
//altough the functionalty is implemented and advertised. Because of
//that, we cant rely on it.
//The problem has been reported to igniterealtime.org since 2006.
//
// if (!MultiUserChat.isServiceEnabled(
// getXmppConnection()
// , jabberProvider.getAccountID().getUserID()))
// {
// throw new OperationNotSupportedException(
// "Chat rooms not supported on server "
// + jabberProvider.getAccountID().getService()
// + " for user "
// + jabberProvider.getAccountID().getUserID());
// }
}
/**
* In case <tt>roomName</tt> does not represent a complete room id, the
* method returns a canonincal chat room name in the following form:
* roomName@muc-servicename.jabserver.com. In case <tt>roomName</tt> is
* already a canonical room name, the method simply returns it without
* changing it.
*
* @param roomName the name of the room that we'd like to "canonize".
*
* @return the canonincal name of the room (which might be equal to
* roomName in case it was already in a canonical format).
*
* @throws OperationFailedException if we fail retrieving the conference
* service name
*/
private String getCanonicalRoomName(String roomName)
throws OperationFailedException
{
if (roomName.indexOf('@') > 0)
return roomName;
Iterator<String> serviceNamesIter = null;
try
{
serviceNamesIter
= MultiUserChat.getServiceNames(getXmppConnection()).iterator();
}
catch (XMPPException ex)
{
logger.error("Failed to retrieve conference service name for user: "
+ jabberProvider.getAccountID().getUserID()
+ " on server: "
+ jabberProvider.getAccountID().getService()
, ex);
throw new OperationFailedException(
"Failed to retrieve conference service name for user: "
+ jabberProvider.getAccountID().getUserID()
+ " on server: "
+ jabberProvider.getAccountID().getService()
, OperationFailedException.GENERAL_ERROR
, ex);
}
if (serviceNamesIter.hasNext())
return roomName + "@" + serviceNamesIter.next();
//hmmmm strange.. no service name returned. we should probably throw an
//exception
throw new OperationFailedException(
"Failed to retrieve MultiUserChat service names."
, OperationFailedException.GENERAL_ERROR);
}
/**
* Adds <tt>chatRoom</tt> to the cache of chat rooms that this operation
* set is handling.
*
* @param chatRoom the <tt>ChatRoom</tt> to cache.
*/
private void cacheChatRoom(ChatRoom chatRoom)
{
this.chatRoomCache.put(chatRoom.getName(), chatRoom);
}
/**
* Returns a reference to the chat room named <tt>chatRoomName</tt> or
* null if the room hasn't been cached yet.
*
* @param chatRoomName the name of the room we're looking for.
*
* @return the <tt>ChatRoomJabberImpl</tt> instance that has been cached
* for <tt>chatRoomName</tt> or null if no such room has been cached so far.
*/
public ChatRoomJabberImpl getChatRoom(String chatRoomName)
{
return (ChatRoomJabberImpl)this.chatRoomCache.get(chatRoomName);
}
/**
* Returns the list of currently joined chat rooms for
* <tt>chatRoomMember</tt>.
* @param chatRoomMember the member we're looking for
* @return a list of all currently joined chat rooms
* @throws OperationFailedException if the operation fails
* @throws OperationNotSupportedException if the operation is not supported
*/
public List<String> getCurrentlyJoinedChatRooms(ChatRoomMember chatRoomMember)
throws OperationFailedException, OperationNotSupportedException
{
assertSupportedAndConnected();
Iterator<String> joinedRoomsIter = MultiUserChat.getJoinedRooms(
getXmppConnection(), chatRoomMember.getContactAddress());
List<String> joinedRooms = new ArrayList<String>();
while (joinedRoomsIter.hasNext())
joinedRooms.add(joinedRoomsIter.next());
return joinedRooms;
}
/**
* Delivers a <tt>ChatRoomInvitationReceivedEvent</tt> to all
* registered <tt>ChatRoomInvitationListener</tt>s.
*
* @param targetChatRoom the room that invitation refers to
* @param inviter the inviter that sent the invitation
* @param reason the reason why the inviter sent the invitation
* @param password the password to use when joining the room
*/
public void fireInvitationEvent(
ChatRoom targetChatRoom,
String inviter,
String reason,
byte[] password)
{
ChatRoomInvitationJabberImpl invitation
= new ChatRoomInvitationJabberImpl( targetChatRoom,
inviter,
reason,
password);
fireInvitationReceived(invitation);
}
/**
* A listener that is fired anytime an invitation to join a MUC room is
* received.
*/
private class SmackInvitationListener
implements InvitationListener
{
/**
* Called when the an invitation to join a MUC room is received.<p>
*
* If the room is password-protected, the invitee will receive a
* password to use to join the room. If the room is members-only, the
* the invitee may be added to the member list.
*
* @param conn the XMPPConnection that received the invitation.
* @param room the room that invitation refers to.
* @param inviter the inviter that sent the invitation.
* (e.g. crone1@shakespeare.lit).
* @param reason the reason why the inviter sent the invitation.
* @param password the password to use when joining the room.
* @param message the message used by the inviter to send the invitation.
*/
public void invitationReceived(Connection conn,
String room, String inviter, String reason,
String password, Message message)
{
ChatRoomJabberImpl chatRoom;
try
{
chatRoom = (ChatRoomJabberImpl) findRoom(room);
if (password != null)
fireInvitationEvent(
chatRoom, inviter, reason, password.getBytes());
else
fireInvitationEvent(
chatRoom, inviter, reason, null);
}
catch (OperationFailedException e)
{
logger.error("Failed to find room with name: " + room, e);
}
catch (OperationNotSupportedException e)
{
logger.error("Failed to find room with name: " + room, e);
}
}
}
/**
* A listener that is fired anytime an invitee declines or rejects an
* invitation.
*/
private class SmackInvitationRejectionListener
implements InvitationRejectionListener
{
/**
* The chat room for this listener.
*/
private ChatRoom chatRoom;
/**
* Creates an instance of <tt>SmackInvitationRejectionListener</tt> and
* passes to it the chat room for which it will listen for rejection
* events.
*
* @param chatRoom chat room for which this intance will listen for
* rejection events
*/
public SmackInvitationRejectionListener(ChatRoom chatRoom)
{
this.chatRoom = chatRoom;
}
/**
* Called when the invitee declines the invitation.
*
* @param invitee the invitee that declined the invitation.
* (e.g. hecate@shakespeare.lit).
* @param reason the reason why the invitee declined the invitation.
*/
public void invitationDeclined(String invitee, String reason)
{
fireInvitationRejectedEvent(chatRoom, invitee, reason);
}
}
/**
* Our listener that will tell us when we're registered to Jabber and the
* smack MultiUserChat is ready to accept us as a listener.
*/
private class RegistrationStateListener
implements RegistrationStateChangeListener
{
/**
* The method is called by a ProtocolProvider implementation whenever
* a change in the registration state of the corresponding provider had
* occurred.
* @param evt ProviderStatusChangeEvent the event describing the status
* change.
*/
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
if (evt.getNewState() == RegistrationState.REGISTERED)
{
if (logger.isDebugEnabled())
logger.debug("adding an Invitation listener to the smack muc");
MultiUserChat.addInvitationListener(
jabberProvider.getConnection(),
new SmackInvitationListener());
}
else if (evt.getNewState() == RegistrationState.UNREGISTERED
|| evt.getNewState() == RegistrationState.CONNECTION_FAILED)
{
// clear cached chatrooms as there are no longer valid
chatRoomCache.clear();
}
else if (evt.getNewState() == RegistrationState.UNREGISTERING)
{
// lets check for joined rooms and leave them
List<ChatRoom> joinedRooms = getCurrentlyJoinedChatRooms();
for(ChatRoom room : joinedRooms)
{
room.leave();
}
}
}
}
/**
* Updates corresponding chat room members when a contact has been modified
* in our contact list.
* @param evt the <tt>SubscriptionEvent</tt> that notified us
*/
public void contactModified(ContactPropertyChangeEvent evt)
{
Contact modifiedContact = evt.getSourceContact();
this.updateChatRoomMembers(modifiedContact);
}
/**
* Updates corresponding chat room members when a contact has been created
* in our contact list.
* @param evt the <tt>SubscriptionEvent</tt> that notified us
*/
public void subscriptionCreated(SubscriptionEvent evt)
{
Contact createdContact = evt.getSourceContact();
this.updateChatRoomMembers(createdContact);
}
/**
* Not interested in this event for our member update purposes.
* @param evt the <tt>SubscriptionEvent</tt> that notified us
*/
public void subscriptionFailed(SubscriptionEvent evt) {}
/**
* Not interested in this event for our member update purposes.
* @param evt the <tt>SubscriptionEvent</tt> that notified us
*/
public void subscriptionMoved(SubscriptionMovedEvent evt) {}
/**
* Updates corresponding chat room members when a contact has been removed
* from our contact list.
* @param evt the <tt>SubscriptionEvent</tt> that notified us
*/
public void subscriptionRemoved(SubscriptionEvent evt)
{
}
/**
* Not interested in this event for our member update purposes.
* @param evt the <tt>SubscriptionEvent</tt> that notified us
*/
public void subscriptionResolved(SubscriptionEvent evt) {}
/**
* Finds all chat room members, which name corresponds to the name of the
* given contact and updates their contact references.
*
* @param contact the contact we're looking correspondences for.
*/
private void updateChatRoomMembers(Contact contact)
{
Enumeration<ChatRoom> chatRooms = chatRoomCache.elements();
while (chatRooms.hasMoreElements())
{
ChatRoomJabberImpl chatRoom =
(ChatRoomJabberImpl) chatRooms.nextElement();
ChatRoomMemberJabberImpl member
= chatRoom.findMemberForNickName(contact.getAddress());
if (member != null)
{
member.setContact(contact);
member.setAvatar(contact.getImage());
}
}
}
}