/*
* 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.irc;
import java.beans.*;
import java.io.*;
import java.util.*;
import net.java.sip.communicator.impl.protocol.irc.exception.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
/**
* Represents a chat channel/room, where multiple chat users could rally and
* communicate in a many-to-many fashion.
*
* @author Stephane Remy
* @author Loic Kempf
* @author Yana Stamcheva
* @author Danny van Heumen
*/
public class ChatRoomIrcImpl
extends AbstractChatRoom
{
/**
* Default channel prefix in case user forgot to include a valid channel
* prefix in the chat room name.
*/
private static final char DEFAULT_CHANNEL_PREFIX = '#';
/**
* Maximum length of an IRC channel name.
*/
private static final int MAXIMUM_LENGTH_OF_CHANNEL_NAME = 200;
/**
* The object used for logging.
*/
private static final Logger LOGGER
= Logger.getLogger(ChatRoomIrcImpl.class);
/**
* The parent protocol service provider.
*/
private final ProtocolProviderServiceIrcImpl parentProvider;
/**
* The name of the chat room.
*/
private final String chatRoomName;
/**
* The subject of the chat room.
*/
private String chatSubject = "";
/**
* list of members of this chatRoom.
*/
private final Hashtable<String, ChatRoomMember> chatRoomMembers
= new Hashtable<String, ChatRoomMember>();
/**
* Listeners that will be notified of changes in member status in the
* room such as member joined, left or being kicked or dropped.
*/
private final Vector<ChatRoomMemberPresenceListener>
memberListeners = new Vector<ChatRoomMemberPresenceListener>();
/**
* Listeners that will be notified of changes in member role in the
* room such as member being granted admin permissions, or revoked admin
* permissions.
*/
private final Vector<ChatRoomMemberRoleListener> memberRoleListeners
= new Vector<ChatRoomMemberRoleListener>();
/**
* Listeners that will be notified of changes in local user role in the
* room such as member being granted administrator permissions, or revoked
* administrator permissions.
*/
private final Vector<ChatRoomLocalUserRoleListener> localUserRoleListeners
= new Vector<ChatRoomLocalUserRoleListener>();
/**
* Listeners that will be notified every time
* a new message is received on this chat room.
*/
private final Vector<ChatRoomMessageListener> messageListeners
= new Vector<ChatRoomMessageListener>();
/**
* Listeners that will be notified every time
* a chat room property has been changed.
*/
private final Vector<ChatRoomPropertyChangeListener> propertyChangeListeners
= new Vector<ChatRoomPropertyChangeListener>();
/**
* Listeners that will be notified every time
* a chat room member property has been changed.
*/
private final Vector<ChatRoomMemberPropertyChangeListener>
memberPropChangeListeners
= new Vector<ChatRoomMemberPropertyChangeListener>();
/**
* The table containing all banned members.
*/
private ArrayList<ChatRoomMember> bannedMembers
= new ArrayList<ChatRoomMember>();
/**
* Indicates if this chat room is a system one (i.e. corresponding to the
* server channel).
*/
private boolean isSystem = false;
/**
* Instance of chat room member that represents the user.
*/
private ChatRoomMemberIrcImpl user = null;
/**
* Creates an instance of <tt>ChatRoomIrcImpl</tt>, by specifying the room
* name and the protocol provider.
*
* @param chatRoomName the name of the chat room
* @param parentProvider the protocol provider
*/
public ChatRoomIrcImpl(final String chatRoomName,
final ProtocolProviderServiceIrcImpl parentProvider)
{
this(chatRoomName, parentProvider, false);
}
/**
* Creates an instance of <tt>ChatRoomIrcImpl</tt>, by specifying the room
* name, the protocol provider and the isPrivate property. Private chat
* rooms are one-to-one chat rooms.
*
* @param chatRoomName the name of the chat room (cannot be null or empty
* string)
* @param parentProvider the protocol provider
* @param isSystem indicates if this chat room is a system room
*/
public ChatRoomIrcImpl(final String chatRoomName,
final ProtocolProviderServiceIrcImpl parentProvider,
final boolean isSystem)
{
if (parentProvider == null)
{
throw new IllegalArgumentException("parentProvider cannot be null");
}
this.parentProvider = parentProvider;
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
this.chatRoomName =
verifyName(connection.getChannelManager().getChannelTypes(),
chatRoomName);
this.isSystem = isSystem;
}
/**
* Verify if the chat room name/identifier meets all the criteria.
*
* @param name chat room name/identifier
* @return returns the chat room name if it is valid
* @throws IllegalArgumentException if name/identifier contains invalid
* characters
*/
private static String verifyName(final Set<Character> channelTypes,
final String name)
{
if (name == null || name.isEmpty()
|| name.length() > MAXIMUM_LENGTH_OF_CHANNEL_NAME)
{
throw new IllegalArgumentException("Invalid chat room name.");
}
final char prefix = name.charAt(0);
// Check for default channel prefix explicitly just in case it isn't
// listed as a channel type.
if (channelTypes.contains(prefix) || prefix == DEFAULT_CHANNEL_PREFIX)
{
for (char c : IrcConnection.SPECIAL_CHARACTERS)
{
if (name.contains("" + c))
{
throw new IllegalArgumentException(
"chat room identifier contains illegal character: " + c);
}
}
return name;
}
else
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Automatically added " + DEFAULT_CHANNEL_PREFIX
+ " channel prefix.");
}
return verifyName(channelTypes, DEFAULT_CHANNEL_PREFIX + name);
}
}
/**
* hashCode implementation for Chat Room.
*
* @return returns hash code for this instance
*/
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + chatRoomName.hashCode();
result = prime * result + parentProvider.hashCode();
return result;
}
/**
* equals implementation for Chat Room.
*
* @param obj other instance
* @return returns true if equal or false if not
*/
@Override
public boolean equals(final Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
ChatRoomIrcImpl other = (ChatRoomIrcImpl) obj;
if (!parentProvider.equals(other.parentProvider))
{
return false;
}
if (!chatRoomName.equals(other.chatRoomName))
{
return false;
}
return true;
}
/**
* Returns the name of this <tt>ChatRoom</tt>.
*
* @return a <tt>String</tt> containing the name of this <tt>ChatRoom</tt>.
*/
public String getName()
{
return chatRoomName;
}
/**
* Returns the identifier of this <tt>ChatRoom</tt>.
*
* @return a <tt>String</tt> containing the identifier of this
* <tt>ChatRoom</tt>.
*/
public String getIdentifier()
{
return chatRoomName;
}
/**
* Adds a <tt>ChatRoomMember</tt> to the list of members of this chat room.
*
* @param memberID the identifier of the member
* @param member the <tt>ChatRoomMember</tt> to add.
*/
protected void addChatRoomMember(final String memberID,
final ChatRoomMember member)
{
chatRoomMembers.put(memberID, member);
}
/**
* Removes a <tt>ChatRoomMember</tt> from the list of members of this chat
* room.
*
* @param memberID the name of the <tt>ChatRoomMember</tt> to remove.
*/
protected void removeChatRoomMember(final String memberID)
{
chatRoomMembers.remove(memberID);
}
/**
* Joins this chat room with the nickname of the local user so that the user
* would start receiving events and messages for it.
*
* @throws OperationFailedException with the corresponding code if an error
* occurs while joining the room.
*/
public void join() throws OperationFailedException
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null || !connection.isConnected())
{
throw new OperationFailedException(
"We are currently not connected to the server.",
OperationFailedException.NETWORK_FAILURE);
}
if (connection.getChannelManager().isJoined(this))
{
throw new OperationFailedException("Channel is already joined.",
OperationFailedException.SUBSCRIPTION_ALREADY_EXISTS);
}
try
{
connection.getChannelManager().join(this);
}
catch (final IllegalArgumentException e)
{
throw new OperationFailedException(e.getMessage(),
OperationFailedException.CHAT_ROOM_NOT_JOINED, e);
}
}
/**
* Joins this chat room so that the user would start receiving events and
* messages for it. The method uses the nickname of the local user and the
* specified password in order to enter the chatroom.
*
* @param password the password to use when authenticating on the chatroom.
* @throws OperationFailedException with the corresponding code if an error
* occurs while joining the room.
*/
public void join(final byte[] password) throws OperationFailedException
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new OperationFailedException(
"We are currently not connected to the server.",
OperationFailedException.NETWORK_FAILURE);
}
if (connection.getChannelManager().isJoined(this))
{
throw new OperationFailedException("Channel is already joined.",
OperationFailedException.SUBSCRIPTION_ALREADY_EXISTS);
}
try
{
connection.getChannelManager().join(this, password.toString());
}
catch (final IllegalArgumentException e)
{
throw new OperationFailedException(e.getMessage(),
OperationFailedException.CHAT_ROOM_NOT_JOINED, e);
}
}
/**
* Joins this chat room with the specified nickname so that the user would
* start receiving events and messages for it. If the chat room already
* contains a user with this nickname, the method would throw an
* OperationFailedException with code IDENTIFICATION_CONFLICT.
*
* The provided nick name is ignored, since IRC does not support nick
* changes limited to a single chat room.
*
* @param nickname the nickname to use.
* @throws OperationFailedException with the corresponding code if an error
* occurs while joining the room.
*/
public void joinAs(final String nickname) throws OperationFailedException
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Not setting nick name upon chat room join, since a "
+ "nick change is not limited to a single chat room.");
}
this.join();
}
/**
* Joins this chat room with the specified nickname and password so that the
* user would start receiving events and messages for it. If the chatroom
* already contains a user with this nickname, the method would throw an
* OperationFailedException with code IDENTIFICATION_CONFLICT.
*
* The provided nick name is ignored, since IRC does not support nick
* changes limited to a single chat room.
*
* @param nickname the nickname to use.
* @param password a password necessary to authenticate when joining the
* room.
* @throws OperationFailedException with the corresponding code if an error
* occurs while joining the room.
*/
public void joinAs(final String nickname, final byte[] password)
throws OperationFailedException
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Not setting nick name upon chat room join, since a "
+ "nick change is not limited to a single chat room.");
}
this.join(password);
}
/**
* Returns true if the local user is currently in the multi user chat (after
* calling one of the {@link #join()} methods).
*
* @return true if currently we're currently in this chat room and false
* otherwise.
*/
public boolean isJoined()
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
return connection != null
&& connection.getChannelManager().isJoined(this);
}
/**
* Leave this chat room. Once this method is called, the user won't be
* listed as a member of the chat room any more and no further chat events
* will be delivered. Depending on the underlying protocol and
* implementation leave() might cause the room to be destroyed if it has
* been created by the local user.
*/
public void leave()
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
return;
}
connection.getChannelManager().leave(this);
this.chatRoomMembers.clear();
}
/**
* Returns the list of banned chat room members.
* @return the list of banned chat room members.
*
* @throws OperationFailedException if we are not joined or we don't have
* enough privileges to obtain the ban list.
*/
public Iterator<ChatRoomMember> getBanList()
throws OperationFailedException
{
return bannedMembers.iterator();
}
/**
* Bans the given <tt>ChatRoomMember</tt>.
*
* @param chatRoomMember the chat room member to ban
* @param reason the reason of the ban
* @throws OperationFailedException if we are not joined or we don't have
* enough privileges to ban a participant.
*/
public void banParticipant(final ChatRoomMember chatRoomMember,
final String reason) throws OperationFailedException
{
if (!(chatRoomMember instanceof ChatRoomMemberIrcImpl))
{
LOGGER
.trace("Cannot ban chat room member that is not an instance of "
+ ChatRoomMemberIrcImpl.class.getCanonicalName());
return;
}
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().banParticipant(this,
(ChatRoomMemberIrcImpl) chatRoomMember, reason);
}
/**
* Kicks the given <tt>ChatRoomMember</tt>.
*
* @param chatRoomMember the chat room member to kick
* @param reason the reason of the kick
* @throws OperationFailedException if we are not joined or we don't have
* enough privileges to kick a participant.
*/
public void kickParticipant(final ChatRoomMember chatRoomMember,
final String reason) throws OperationFailedException
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().kickParticipant(this, chatRoomMember,
reason);
}
/**
* Returns the <tt>ChatRoomConfigurationForm</tt> containing all
* configuration properties for this chat room. If the user doesn't have
* permissions to see and change chat room configuration an
* <tt>OperationFailedException</tt> is thrown.
*
* @return the <tt>ChatRoomConfigurationForm</tt> containing all
* configuration properties for this chat room
* @throws OperationFailedException if the user doesn't have
* permissions to see and change chat room configuration
*/
public ChatRoomConfigurationForm getConfigurationForm()
throws OperationFailedException
{
throw new OperationFailedException(
"The configuration form is not yet implemented for irc.",
OperationFailedException.GENERAL_ERROR);
}
/**
* Adds <tt>listener</tt> to the list of listeners registered to receive
* events upon modification of chat room properties such as its subject for
* example.
*
* @param listener ChatRoomChangeListener
*/
public void addPropertyChangeListener(
final ChatRoomPropertyChangeListener listener)
{
synchronized (propertyChangeListeners)
{
if (!propertyChangeListeners.contains(listener))
{
propertyChangeListeners.add(listener);
}
}
}
/**
* Removes <tt>listener</tt> from the list of listeners current
* registered for chat room modification events.
*
* @param listener the <tt>ChatRoomChangeListener</tt> to remove.
*/
public void removePropertyChangeListener(
final ChatRoomPropertyChangeListener listener)
{
synchronized (propertyChangeListeners)
{
propertyChangeListeners.remove(listener);
}
}
/**
* Adds the given <tt>listener</tt> to the list of listeners registered to
* receive events upon modification of chat room member properties such as
* its nickname being changed for example.
*
* @param listener the <tt>ChatRoomMemberPropertyChangeListener</tt>
* that is to be registered for <tt>ChatRoomMemberPropertyChangeEvent</tt>s.
*/
public void addMemberPropertyChangeListener(
final ChatRoomMemberPropertyChangeListener listener)
{
synchronized (memberPropChangeListeners)
{
if (!memberPropChangeListeners.contains(listener))
{
memberPropChangeListeners.add(listener);
}
}
}
/**
* Removes the given <tt>listener</tt> from the list of listeners currently
* registered for chat room member property change events.
*
* @param listener the <tt>ChatRoomMemberPropertyChangeListener</tt> to
* remove.
*/
public void removeMemberPropertyChangeListener(
final ChatRoomMemberPropertyChangeListener listener)
{
synchronized (memberPropChangeListeners)
{
memberPropChangeListeners.remove(listener);
}
}
/**
* Adds a listener that will be notified of changes of a member role in the
* room such as being granted operator.
*
* @param listener a member role listener.
*/
public void addMemberRoleListener(final ChatRoomMemberRoleListener listener)
{
synchronized (memberRoleListeners)
{
if (!memberRoleListeners.contains(listener))
{
memberRoleListeners.add(listener);
}
}
}
/**
* Removes a listener that was being notified of changes of a member role in
* this chat room such as us being granded operator.
*
* @param listener a member role listener.
*/
public void removeMemberRoleListener(
final ChatRoomMemberRoleListener listener)
{
synchronized (memberRoleListeners)
{
if (memberRoleListeners.contains(listener))
{
memberRoleListeners.remove(listener);
}
}
}
/**
* Adds a listener that will be notified of changes in our role in the room
* such as us being granded operator.
*
* @param listener a local user role listener.
*/
public void addLocalUserRoleListener(
final ChatRoomLocalUserRoleListener listener)
{
synchronized (localUserRoleListeners)
{
if (!localUserRoleListeners.contains(listener))
{
localUserRoleListeners.add(listener);
}
}
}
/**
* Removes a listener that was being notified of changes in our role in this
* chat room such as us being granted operator.
*
* @param listener a local user role listener.
*/
public void removelocalUserRoleListener(
final ChatRoomLocalUserRoleListener listener)
{
synchronized (localUserRoleListeners)
{
if (localUserRoleListeners.contains(listener))
{
localUserRoleListeners.remove(listener);
}
}
}
/**
* Returns the last known room subject/theme or <tt>null</tt> if the user
* hasn't joined the room or the room does not have a subject yet.
* <p>
* To be notified every time the room's subject change you should add a
* <tt>ChatRoomPropertyChangelistener</tt> to this room.
* <p>
*
* To change the room's subject use {@link #setSubject(String)}.
*
* @return the room subject or <tt>null</tt> if the user hasn't joined the
* room or the room does not have a subject yet.
*/
public String getSubject()
{
return chatSubject;
}
/**
* Sets the subject of this chat room. If the user does not have the right
* to change the room subject, or the protocol does not support this, or the
* operation fails for some other reason, the method throws an
* <tt>OperationFailedException</tt> with the corresponding code.
*
* @param subject the new subject that we'd like this room to have
* @throws OperationFailedException thrown if the user is not joined to the
* channel or if he/she doesn't have enough privileges to change the
* topic or if the topic is null.
*/
public void setSubject(final String subject)
throws OperationFailedException
{
try
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().setSubject(this, subject);
}
catch (RuntimeException e)
{
if (e.getCause() instanceof IOException)
{
throw new OperationFailedException("Failed to change subject.",
OperationFailedException.NETWORK_FAILURE, e.getCause());
}
throw new OperationFailedException("Failed to change subject.",
OperationFailedException.GENERAL_ERROR, e);
}
}
/**
* Returns the local user's nickname in the context of this chat room or
* <tt>null</tt> if not currently joined.
*
* @return the nickname currently being used by the local user
*/
public String getUserNickname()
{
// User's nick name is determined by the server connection, not the
// individual chat rooms.
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
return connection.getIdentityManager().getNick();
}
/**
* Changes the the local user's nickname in the context of this chat room.
* If the operation is not supported by the underlying implementation, the
* method throws an OperationFailedException with the corresponding code.
*
* @param nickName the new nickname within the room.
*
* @throws OperationFailedException if the setting the new nickname changes
* for some reason.
*/
@Override
public void setUserNickname(final String nickName)
throws OperationFailedException
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new OperationFailedException(
"IRC connection is not established.",
OperationFailedException.NETWORK_FAILURE);
}
connection.getIdentityManager().setNick(nickName);
}
/**
* Adds a listener that will be notified of changes in our status in the
* room such as us being kicked, banned, or granted admin permissions.
*
* @param listener a participant status listener.
*/
public void addMemberPresenceListener(
final ChatRoomMemberPresenceListener listener)
{
synchronized (memberListeners)
{
if (!memberListeners.contains(listener))
{
memberListeners.add(listener);
}
}
}
/**
* Removes a listener that was being notified of changes in the status of
* other chat room participants such as users being kicked, banned, or
* granted admin permissions.
*
* @param listener a participant status listener.
*/
public void removeMemberPresenceListener(
final ChatRoomMemberPresenceListener listener)
{
synchronized (memberListeners)
{
memberListeners.remove(listener);
}
}
/**
* Registers <tt>listener</tt> so that it would receive events every time
* a new message is received on this chat room.
*
* @param listener a <tt>MessageListener</tt> that would be notified every
* time a new message is received on this chat room.
*/
public void addMessageListener(final ChatRoomMessageListener listener)
{
synchronized (messageListeners)
{
if (!messageListeners.contains(listener))
{
messageListeners.add(listener);
}
}
}
/**
* Removes <tt>listener</tt> so that it won't receive any further message
* events from this room.
*
* @param listener the <tt>MessageListener</tt> to remove from this room
*/
public void removeMessageListener(final ChatRoomMessageListener listener)
{
synchronized (messageListeners)
{
if (messageListeners.contains(listener))
{
messageListeners.remove(messageListeners.indexOf(listener));
}
}
}
/**
* Returns the <tt>ChatRoomMember</tt> corresponding to the given member id.
* If no member is found for the given id, returns NULL.
*
* @param memberID the identifier of the member
* @return the <tt>ChatRoomMember</tt> corresponding to the given member id.
*/
public ChatRoomMember getChatRoomMember(final String memberID)
{
return chatRoomMembers.get(memberID);
}
/**
* Removes all chat room members from the list.
*/
protected void clearChatRoomMemberList()
{
synchronized (chatRoomMembers)
{
chatRoomMembers.clear();
}
}
/**
* Invites another user to this room. If we're not joined nothing will
* happen.
*
* @param userAddress the address of the user to invite to the room.(one may
* also invite users not on their contact list).
* @param reason a reason, subject, or welcome message that would tell the
* the user why they are being invited.
*/
@Override
public void invite(final String userAddress, final String reason)
{
// TODO Check if channel status is invite-only (+i). If this is the
// case, user has to be channel operator in order to be able to invite
// some-one.
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().invite(userAddress, this);
}
/**
* Returns a <tt>List</tt> of <tt>ChatRoomMembers</tt>s corresponding to all
* members currently participating in this room.
*
* @return a <tt>List</tt> of <tt>Contact</tt> corresponding to all room
* members.
*/
public List<ChatRoomMember> getMembers()
{
return new ArrayList<ChatRoomMember>(chatRoomMembers.values());
}
/**
* Returns the number of participants that are currently in this chat room.
*
* @return the number of <tt>Contact</tt>s, currently participating in this
* room.
*/
public int getMembersCount()
{
return chatRoomMembers.size();
}
/**
* Create a Message instance for sending arbitrary MIME-encoding content.
*
* @param content content value
* @param contentType the MIME-type for <tt>content</tt>
* @param contentEncoding encoding used for <tt>content</tt>
* @param subject a <tt>String</tt> subject or <tt>null</tt> for now
* subject.
* @return the newly created message.
*/
@Override
public Message createMessage(final byte[] content, final String contentType,
final String contentEncoding, final String subject)
{
Message msg =
new MessageIrcImpl(new String(content), contentType,
contentEncoding, subject);
return msg;
}
/**
* Create a Message instance for sending a simple text messages with default
* (text/plain) content type and encoding.
*
* @param messageText the string content of the message.
* @return Message the newly created message
*/
@Override
public Message createMessage(final String messageText)
{
Message mess = new MessageIrcImpl(
messageText,
OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE,
OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING,
null);
return mess;
}
/**
* Sends the <tt>message</tt> to the destination indicated by the
* <tt>to</tt> contact.
*
* @param message the <tt>Message</tt> to send.
* @throws OperationFailedException if the underlying stack is not
* registered or initialized or if the chat room is not joined.
*/
@Override
public void sendMessage(final Message message)
throws OperationFailedException
{
assertConnected();
String[] splitMessages = message.getContent().split("\n");
String messagePortion = null;
for (int i = 0; i < splitMessages.length; i++)
{
messagePortion = splitMessages[i];
// As we only send one message per line, we ignore empty lines in
// the incoming multi line message.
if (messagePortion.equals("\n") || messagePortion.matches("[\\ ]*"))
{
continue;
}
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
if (((MessageIrcImpl) message).isCommand())
{
try
{
connection.getMessageManager()
.command(this, messagePortion);
this.fireMessageReceivedEvent(message, this.user,
new Date(),
ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
}
catch (final UnsupportedCommandException e)
{
this.fireMessageDeliveryFailedEvent(
ChatRoomMessageDeliveryFailedEvent
.UNSUPPORTED_OPERATION,
e.getMessage(), new Date(), message);
}
catch (BadCommandException e)
{
LOGGER.error("An error occurred while constructing "
+ "the command. This is most likely due to a bug "
+ "in the implementation of the command. Message: "
+ message + "'", e);
this.fireMessageDeliveryFailedEvent(
ChatRoomMessageDeliveryFailedEvent.INTERNAL_ERROR,
"Command cannot be executed. This is most likely due "
+ "to a bug in the implementation.", new Date(),
message);
}
catch (BadCommandInvocationException e)
{
StringBuilder helpText = new StringBuilder();
if (e.getCause() != null) {
helpText.append(e.getCause().getMessage());
helpText.append('\n');
}
helpText.append(e.getHelp());
MessageIrcImpl helpMessage =
new MessageIrcImpl(
helpText.toString(),
OperationSetBasicInstantMessaging
.DEFAULT_MIME_TYPE,
OperationSetBasicInstantMessaging
.DEFAULT_MIME_ENCODING,
"Command usage:");
this.fireMessageReceivedEvent(helpMessage, this.user,
new Date(),
MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
}
}
else
{
connection.getMessageManager().message(this, messagePortion);
this.fireMessageDeliveredEvent(new MessageIrcImpl(
messagePortion, message.getContentType(), message
.getEncoding(), message.getSubject()));
}
}
}
/**
* Returns the protocol provider service that created us.
*
* @return the protocol provider service that created us.
*/
public ProtocolProviderService getParentProvider()
{
return parentProvider;
}
/**
* Utility method throwing an exception if the stack is not properly
* initialized.
*
* @throws java.lang.IllegalStateException if the underlying stack is not
* registered and initialized.
*/
private void assertConnected() throws IllegalStateException
{
if (parentProvider == null)
{
throw new IllegalStateException(
"The provider must be non-null and signed on the "
+ "service before being able to communicate.");
}
if (!parentProvider.isRegistered())
{
throw new IllegalStateException(
"The provider must be signed on the service before "
+ "being able to communicate.");
}
}
/**
* Notifies all interested listeners that a
* <tt>ChatRoomMessageDeliveredEvent</tt> has been fired.
*
* @param message the delivered message
*/
private void fireMessageDeliveredEvent(final Message message)
{
int eventType
= ChatRoomMessageDeliveredEvent.CONVERSATION_MESSAGE_DELIVERED;
MessageIrcImpl msg = (MessageIrcImpl) message;
if (msg.isAction())
{
eventType = ChatRoomMessageDeliveredEvent.ACTION_MESSAGE_DELIVERED;
if (msg.getContent().indexOf(' ') != -1)
{
msg.setContent(
msg.getContent()
.substring(message.getContent().indexOf(' ')));
}
}
ChatRoomMessageDeliveredEvent msgDeliveredEvt
= new ChatRoomMessageDeliveredEvent(this,
new Date(),
msg,
eventType);
Iterable<ChatRoomMessageListener> listeners;
synchronized (messageListeners)
{
listeners
= new ArrayList<ChatRoomMessageListener>(messageListeners);
}
for (ChatRoomMessageListener listener : listeners)
{
try
{
listener.messageDelivered(msgDeliveredEvt);
}
catch (RuntimeException e)
{
LOGGER.error(String.format(
"Listener '%s' threw a runtime exception during execution."
+ " This is probably due to a bug in the listener's "
+ "implementation.",
listener.getClass().getCanonicalName()),
e);
}
}
}
/**
* Notifies all interested listeners that a
* <tt>ChatRoomMessageReceivedEvent</tt> has been fired.
*
* @param message the received message
* @param fromMember the <tt>ChatRoomMember</tt>, which is the sender of the
* message
* @param date the time at which the message has been received
* @param eventType the type of the received event. One of the
* XXX_MESSAGE_RECEIVED constants declared in the
* <tt>ChatRoomMessageReceivedEvent</tt> class.
*/
public void fireMessageReceivedEvent(final Message message,
final ChatRoomMember fromMember, final Date date, final int eventType)
{
ChatRoomMessageReceivedEvent event =
new ChatRoomMessageReceivedEvent(this, fromMember, date, message,
eventType);
Iterable<ChatRoomMessageListener> listeners;
synchronized (messageListeners)
{
listeners
= new ArrayList<ChatRoomMessageListener>(messageListeners);
}
for (ChatRoomMessageListener listener : listeners)
{
try
{
listener.messageReceived(event);
}
catch (RuntimeException e)
{
LOGGER.error(String.format(
"Listener '%s' threw a runtime exception during execution."
+ " This is probably due to a bug in the listener's "
+ "implementation.",
listener.getClass().getCanonicalName()),
e);
}
}
}
/**
* Notifies interested listeners that a message delivery has failed.
*
* @param errorCode the type of error that occurred
* @param reason the reason of delivery failure
* @param date the date the event was received
* @param message the message that was failed to be delivered
*/
public void fireMessageDeliveryFailedEvent(final int errorCode,
final String reason, final Date date, final Message message)
{
final ChatRoomMessageDeliveryFailedEvent event =
new ChatRoomMessageDeliveryFailedEvent(this, null, errorCode,
reason, date, message);
final Iterable<ChatRoomMessageListener> listeners;
synchronized (messageListeners)
{
listeners
= new ArrayList<ChatRoomMessageListener>(messageListeners);
}
for (final ChatRoomMessageListener listener : listeners)
{
try
{
listener.messageDeliveryFailed(event);
}
catch (RuntimeException e)
{
LOGGER.error(String.format(
"Listener '%s' threw a runtime exception during execution."
+ " This is probably due to a bug in the listener's "
+ "implementation.",
listener.getClass().getCanonicalName()),
e);
}
}
}
/**
* Delivers the specified event to all registered property change listeners.
*
* @param evt the <tt>PropertyChangeEvent</tt> that we'd like delivered to
* all registered property change listeners.
*/
public void firePropertyChangeEvent(final PropertyChangeEvent evt)
{
Iterable<ChatRoomPropertyChangeListener> listeners;
synchronized (propertyChangeListeners)
{
listeners
= new ArrayList<ChatRoomPropertyChangeListener>(
propertyChangeListeners);
}
for (ChatRoomPropertyChangeListener listener : listeners)
{
if (evt instanceof ChatRoomPropertyChangeEvent)
{
listener.chatRoomPropertyChanged(
(ChatRoomPropertyChangeEvent) evt);
}
else if (evt instanceof ChatRoomPropertyChangeFailedEvent)
{
listener.chatRoomPropertyChangeFailed(
(ChatRoomPropertyChangeFailedEvent) evt);
}
}
}
/**
* Delivers the specified event to all registered property change listeners.
*
* @param evt the <tt>ChatRoomMemberPropertyChangeEvent</tt> that we'd like
* deliver to all registered member property change listeners.
*/
public void fireMemberPropertyChangeEvent(
final ChatRoomMemberPropertyChangeEvent evt)
{
Iterable<ChatRoomMemberPropertyChangeListener> listeners;
synchronized (memberPropChangeListeners)
{
listeners
= new ArrayList<ChatRoomMemberPropertyChangeListener>(
memberPropChangeListeners);
}
for (ChatRoomMemberPropertyChangeListener listener : listeners)
{
listener.chatRoomPropertyChanged(evt);
}
}
/**
* Creates the corresponding ChatRoomMemberPresenceChangeEvent and notifies
* all <tt>ChatRoomMemberPresenceListener</tt>s that a ChatRoomMember has
* joined or left this <tt>ChatRoom</tt>.
*
* @param member the <tt>ChatRoomMember</tt> that this event is about
* @param actorMember a member that act in the event (for example the kicker
* in a member kicked event)
* @param eventID the identifier of the event
* @param eventReason the reason of the event
*/
public void fireMemberPresenceEvent(final ChatRoomMember member,
final ChatRoomMember actorMember, final String eventID,
final String eventReason)
{
// First update local state w.r.t. member presence change
if (eventID == ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED)
{
addChatRoomMember(member.getContactAddress(), member);
}
else
{
removeChatRoomMember(member.getContactAddress());
}
ChatRoomMemberPresenceChangeEvent evt;
if (actorMember != null)
{
evt = new ChatRoomMemberPresenceChangeEvent(
this, member, actorMember, eventID, eventReason);
}
else
{
evt = new ChatRoomMemberPresenceChangeEvent(
this, member, eventID, eventReason);
}
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Will dispatch the following ChatRoom event: " + evt);
}
Iterable<ChatRoomMemberPresenceListener> listeners;
synchronized (memberListeners)
{
listeners
= new ArrayList<ChatRoomMemberPresenceListener>(
memberListeners);
}
for (ChatRoomMemberPresenceListener listener : listeners)
{
listener.memberPresenceChanged(evt);
}
}
/**
* Creates the corresponding ChatRoomMemberRoleChangeEvent and notifies
* all <tt>ChatRoomMemberRoleListener</tt>s that a ChatRoomMember has
* changed his role in this <tt>ChatRoom</tt>.
*
* @param member the <tt>ChatRoomMember</tt> that this event is about
* @param newRole the new role of the given member
*/
public void fireMemberRoleEvent(final ChatRoomMember member,
final ChatRoomMemberRole newRole)
{
member.setRole(newRole);
ChatRoomMemberRole previousRole = member.getRole();
ChatRoomMemberRoleChangeEvent evt
= new ChatRoomMemberRoleChangeEvent(this,
member,
previousRole,
newRole);
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("Will dispatch the following ChatRoom event: " + evt);
}
Iterable<ChatRoomMemberRoleListener> listeners;
synchronized (memberRoleListeners)
{
listeners
= new ArrayList<ChatRoomMemberRoleListener>(
memberRoleListeners);
}
for (ChatRoomMemberRoleListener listener : listeners)
{
listener.memberRoleChanged(evt);
}
}
/**
* Notify all <tt>ChatRoomLocalUserRoleListener</tt>s that the local user's
* role has been changed in this <tt>ChatRoom</tt>.
*
* @param event the event that describes the local user's role change
*/
public void fireLocalUserRoleChangedEvent(
final ChatRoomLocalUserRoleChangeEvent event)
{
ArrayList<ChatRoomLocalUserRoleListener> listeners;
synchronized (localUserRoleListeners)
{
listeners =
new ArrayList<ChatRoomLocalUserRoleListener>(
localUserRoleListeners);
}
for (ChatRoomLocalUserRoleListener listener : listeners)
{
listener.localUserRoleChanged(event);
}
}
/**
* Indicates whether or not this chat room is corresponding to a server
* channel.
*
* @return <code>true</code> to indicate that this chat room is
* corresponding to a server channel, <code>false</code> - otherwise.
*/
@Override
public boolean isSystem()
{
return isSystem;
}
/**
* Sets whether or not this chat room is corresponding to a server
* channel.
*
* @param isSystem <code>true</code> to indicate that this chat room is
* corresponding to a server channel, <code>false</code> - otherwise.
*/
protected void setSystem(final boolean isSystem)
{
this.isSystem = isSystem;
}
/**
* Sets the subject obtained from the server once we're connected.
*
* @param subject the subject to set
*/
protected void setSubjectFromServer(final String subject)
{
this.chatSubject = subject;
}
/**
* Determines whether this chat room should be stored in the configuration
* file or not. If the chat room is persistent it still will be shown after
* a restart in the chat room list. A non-persistent chat room will be only
* in the chat room list until the the program is running.
*
* @return true if this chat room is persistent, false otherwise
*/
@Override
public boolean isPersistent()
{
return true;
}
/**
* Returns the local user role.
* @return the local user role
*/
@Override
public ChatRoomMemberRole getUserRole()
{
if (this.user == null)
{
LOGGER.trace("User's chat room member instance is not set yet. "
+ "Assuming default role SILENT_MEMBER.");
return ChatRoomMemberRole.SILENT_MEMBER;
}
return this.user.getRole();
}
/**
* Method for setting chat room member instance representing the user.
*
* @param user instance representing the user. This instance cannot be null.
*/
void setLocalUser(final ChatRoomMemberIrcImpl user)
{
if (user == null)
{
throw new IllegalArgumentException("user cannot be null");
}
this.user = user;
}
/**
* Sets the local user role.
*
* No implementation is necessary for this. IRC server manages permissions.
* If a new chat room is created then user will automatically receive the
* appropriate role.
*
* @param role the role to set
* @throws OperationFailedException if the operation don't succeed
*/
@Override
public void setLocalUserRole(final ChatRoomMemberRole role)
throws OperationFailedException
{
}
/**
* Grants admin role to the participant given by <tt>address</tt>.
* @param address the address of the participant to grant admin role to
*/
@Override
public void grantAdmin(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().grant(this, address, Mode.OPERATOR);
}
/**
* Grants membership role to the participant given by <tt>address</tt>.
* @param address the address of the participant to grant membership role to
*/
@Override
public void grantMembership(final String address)
{
// TODO currently Voice == Membership.
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().grant(this, address, Mode.VOICE);
}
/**
* Grants moderator role to the participant given by <tt>address</tt>.
* @param address the address of the participant to grant moderator role to
*/
@Override
public void grantModerator(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().grant(this, address, Mode.HALFOP);
}
/**
* Grants ownership role to the participant given by <tt>address</tt>.
* @param address the address of the participant to grant ownership role to
*/
@Override
public void grantOwnership(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().grant(this, address, Mode.OWNER);
}
/**
* Grants voice to the participant given by <tt>address</tt>.
* @param address the address of the participant to grant voice to
*/
@Override
public void grantVoice(final String address)
{
// TODO currently Voice == Membership.
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().grant(this, address, Mode.VOICE);
}
/**
* Revokes the admin role for the participant given by <tt>address</tt>.
* @param address the address of the participant to revoke admin role for
*/
@Override
public void revokeAdmin(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().revoke(this, address, Mode.OPERATOR);
}
/**
* Revokes the membership role for the participant given by <tt>address</tt>
* .
*
* @param address the address of the participant to revoke membership role
* for
*/
@Override
public void revokeMembership(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().revoke(this, address, Mode.VOICE);
}
/**
* Revokes the moderator role for the participant given by <tt>address</tt>.
* @param address the address of the participant to revoke moderator role
* for
*/
@Override
public void revokeModerator(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().revoke(this, address, Mode.HALFOP);
}
/**
* Revokes the ownership role for the participant given by <tt>address</tt>.
* @param address the address of the participant to revoke ownership role
* for
*/
@Override
public void revokeOwnership(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().revoke(this, address, Mode.OWNER);
}
/**
* Revokes the voice for the participant given by <tt>address</tt>.
* @param address the address of the participant to revoke voice for
*/
@Override
public void revokeVoice(final String address)
{
final IrcConnection connection =
this.parentProvider.getIrcStack().getConnection();
if (connection == null)
{
throw new IllegalStateException("Connection is not available.");
}
connection.getChannelManager().revoke(this, address, Mode.VOICE);
}
/**
* {@inheritDoc}
*
* Not implemented.
*/
@Override
public ConferenceDescription publishConference(
final ConferenceDescription cd, final String name)
{
return null;
}
/**
* Find the Contact instance corresponding to the specified chat room
* member. Since every chat room member is also a private contact, we will
* create an instance if it cannot be found.
*
* @param name nick name of the chat room member
* @return returns Contact instance corresponding to specified chat room
* member
*/
@Override
public Contact getPrivateContactByNickname(final String name)
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Getting private contact for nick name '" + name
+ "'.");
}
// TODO Also register contact address as interesting contact for
// presence status updates at IrcConnection.PresenceManager.
return this.parentProvider.getPersistentPresence()
.findOrCreateContactByID(name);
}
/**
* IRC does not provide continuous presence status updates, so no
* implementation is necessary.
*
* @param nickname nick name to look up
*/
@Override
public void updatePrivateContactPresenceStatus(final String nickname)
{
}
/**
* IRC does not provide continuous presence status updates, so no
* implementation is necessary.
*
* @param sourceContact contact to look up
*/
@Override
public void updatePrivateContactPresenceStatus(final Contact sourceContact)
{
}
/**
* IRC chat rooms cannot be destroyed. That is the way IRC works and there
* is no need to cause a panic, so just return true.
*
* @param reason the reason for destroying.
* @param alternateAddress the alternate address
* @return <tt>true</tt> if the room is destroyed.
*/
public boolean destroy(final String reason, final String alternateAddress)
{
return true;
}
/**
* Returns the ids of the users that has the member role in the room. When
* the room is member only, this are the users allowed to join.
*
* @return the ids of the users that has the member role in the room.
*/
@Override
public List<String> getMembersWhiteList()
{
return new ArrayList<String>();
}
/**
* Changes the list of users that has role member for this room.
* When the room is member only, this are the users allowed to join.
* @param members the ids of user to have member role.
*/
@Override
public void setMembersWhiteList(final List<String> members)
{
}
/**
* Update the subject for this chat room.
*
* @param subject the subject
*/
void updateSubject(final String subject)
{
if (this.chatSubject.equals(subject))
{
return;
}
final String previous =
this.chatSubject == null ? "" : this.chatSubject;
this.chatSubject = subject;
ChatRoomPropertyChangeEvent topicChangeEvent =
new ChatRoomPropertyChangeEvent(this,
ChatRoomPropertyChangeEvent.CHAT_ROOM_SUBJECT, previous,
subject);
firePropertyChangeEvent(topicChangeEvent);
}
/**
* Update the ChatRoomMember instance. When the nick changes, the chat room
* member is still stored under the old nick. Find the instance under its
* old nick and reinsert it into the map according to the current nick name.
*
* @param oldName The old nick name under which the member instance is
* currently stored.
*/
void updateChatRoomMemberName(final String oldName)
{
synchronized (this.chatRoomMembers)
{
ChatRoomMember member = this.chatRoomMembers.remove(oldName);
if (member != null)
{
this.chatRoomMembers.put(member.getContactAddress(), member);
}
}
}
}