/*
* SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.protocol.jabber;
import java.util.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.keepalive.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.version.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.Message;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.event.MessageListener;
import net.java.sip.communicator.service.protocol.jabberconstants.*;
import net.java.sip.communicator.util.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.provider.*;
import org.jivesoftware.smack.util.*;
import org.jivesoftware.smackx.*;
/**
* A straightforward implementation of the basic instant messaging operation
* set.
*
* @author Damian Minkov
*/
public class OperationSetBasicInstantMessagingJabberImpl
implements OperationSetBasicInstantMessaging
{
private static final Logger logger =
Logger.getLogger(OperationSetBasicInstantMessagingJabberImpl.class);
/**
* KeepAlive interval for sending packets
*/
private static final long KEEPALIVE_INTERVAL = 180000l; // 3 minutes
/**
* The interval after which a packet is considered to be lost
*/
private static final long KEEPALIVE_WAIT = 20000l;
private boolean keepAliveEnabled = false;
/**
* The task sending packets
*/
private KeepAliveSendTask keepAliveSendTask = null;
/**
* The timer executing tasks on specified intervals
*/
private Timer keepAliveTimer = new Timer();
/**
* The queue holding the received packets
*/
private LinkedList receivedKeepAlivePackets = new LinkedList();
private int failedKeepalivePackets = 0;
/**
* A list of listeneres registered for message events.
*/
private Vector messageListeners = new Vector();
/**
* The provider that created us.
*/
private ProtocolProviderServiceJabberImpl jabberProvider = null;
/**
* A reference to the persistent presence operation set that we use
* to match incoming messages to Contacts and vice versa.
*/
private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null;
/**
* Creates an instance of this operation set.
* @param provider a ref to the ProtocolProviderServiceImpl
* that created us and that we'll use for retrieving the underlying aim
* connection.
*/
OperationSetBasicInstantMessagingJabberImpl(
ProtocolProviderServiceJabberImpl provider)
{
this.jabberProvider = provider;
provider.addRegistrationStateChangeListener(new RegistrationStateListener());
// register the KeepAlive Extension in the smack library
ProviderManager.getInstance().addIQProvider(KeepAliveEventProvider.ELEMENT_NAME,
KeepAliveEventProvider.NAMESPACE,
new KeepAliveEventProvider());
}
/**
* Registeres a MessageListener with this operation set so that it gets
* notifications of successful message delivery, failure or reception of
* incoming messages..
*
* @param listener the MessageListener to register.
*/
public void addMessageListener(MessageListener listener)
{
synchronized(messageListeners)
{
if(!messageListeners.contains(listener))
{
this.messageListeners.add(listener);
}
}
}
/**
* Unregisteres listener so that it won't receive any further
* notifications upon successful message delivery, failure or reception of
* incoming messages..
*
* @param listener the MessageListener to unregister.
*/
public void removeMessageListener(MessageListener listener)
{
synchronized(messageListeners)
{
this.messageListeners.remove(listener);
}
}
/**
* Create a Message instance for sending arbitrary MIME-encoding content.
*
* @param content content value
* @param contentType the MIME-type for content
* @param contentEncoding encoding used for content
* @param subject a String subject or null for now subject.
* @return the newly created message.
*/
public Message createMessage(byte[] content, String contentType,
String contentEncoding, String subject)
{
return new MessageJabberImpl(new String(content), contentType
, contentEncoding, subject);
}
/**
* 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
*/
public Message createMessage(String messageText)
{
return new MessageJabberImpl(messageText, DEFAULT_MIME_TYPE
, DEFAULT_MIME_ENCODING, null);
}
/**
* Determines wheter the protocol provider (or the protocol itself) support
* sending and receiving offline messages. Most often this method would
* return true for protocols that support offline messages and false for
* those that don't. It is however possible for a protocol to support these
* messages and yet have a particular account that does not (i.e. feature
* not enabled on the protocol server). In cases like this it is possible
* for this method to return true even when offline messaging is not
* supported, and then have the sendMessage method throw an
* OperationFailedException with code - OFFLINE_MESSAGES_NOT_SUPPORTED.
*
* @return true if the protocol supports offline messages and
* false otherwise.
*/
public boolean isOfflineMessagingSupported()
{
return true;
}
/**
* Determines wheter the protocol supports the supplied content type
*
* @param contentType the type we want to check
* @return true if the protocol supports it and
* false otherwise.
*/
public boolean isContentTypeSupported(String contentType)
{
if(contentType.equals(DEFAULT_MIME_TYPE))
return true;
else
return false;
}
/**
* Sends the message to the destination indicated by the
* to contact.
*
* @param to the Contact to send message to
* @param message the Message to send.
* @throws java.lang.IllegalStateException if the underlying stack is
* not registered and initialized.
* @throws java.lang.IllegalArgumentException if to is not an
* instance of ContactImpl.
*/
public void sendInstantMessage(Contact to, Message message)
throws IllegalStateException, IllegalArgumentException
{
if( !(to instanceof ContactJabberImpl) )
throw new IllegalArgumentException(
"The specified contact is not a Jabber contact."
+ to);
try
{
assertConnected();
org.jivesoftware.smack.MessageListener msgListener =
new org.jivesoftware.smack.MessageListener() {
public void processMessage(Chat arg0, org.jivesoftware.smack.packet.Message arg1) {
}
};
Chat chat = jabberProvider.getConnection().getChatManager().createChat(
to.getAddress(), msgListener
);
org.jivesoftware.smack.packet.Message msg =
new org.jivesoftware.smack.packet.Message();
msg.setBody(message.getContent());
msg.addExtension(new Version());
MessageEventManager.
addNotificationsRequests(msg, true, false, false, true);
chat.sendMessage(msg);
MessageDeliveredEvent msgDeliveredEvt
= new MessageDeliveredEvent(
message, to, new Date());
fireMessageEvent(msgDeliveredEvt);
}
catch (XMPPException ex)
{
logger.error("message not send", ex);
}
}
/**
* 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 (jabberProvider == null)
throw new IllegalStateException(
"The provider must be non-null and signed on the "
+"service before being able to communicate.");
if (!jabberProvider.isRegistered())
throw new IllegalStateException(
"The provider must be signed on the service before "
+"being able to communicate.");
}
/**
* Our listener that will tell us when we're registered to
*/
private class RegistrationStateListener
implements RegistrationStateChangeListener
{
/**
* The method is called by a ProtocolProvider implementation whenver
* 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)
{
logger.debug("The provider changed state from: "
+ evt.getOldState()
+ " to: " + evt.getNewState());
if (evt.getNewState() == RegistrationState.REGISTERED)
{
opSetPersPresence = (OperationSetPersistentPresenceJabberImpl)
jabberProvider.getSupportedOperationSets()
.get(OperationSetPersistentPresence.class.getName());
jabberProvider.getConnection().addPacketListener(
new SmackMessageListener(),
new AndFilter(
new PacketFilter[]{new GroupMessagePacketFilter(),
new PacketTypeFilter(
org.jivesoftware.smack.packet.Message.class)}));
// run keepalive thread
if(keepAliveSendTask == null && keepAliveEnabled)
{
jabberProvider.getConnection().addPacketListener(
new KeepalivePacketListener(),
new PacketTypeFilter(
KeepAliveEvent.class));
keepAliveSendTask = new KeepAliveSendTask();
keepAliveTimer.scheduleAtFixedRate(
keepAliveSendTask, KEEPALIVE_INTERVAL, KEEPALIVE_INTERVAL);
}
}
}
}
/**
* Delivers the specified event to all registered message listeners.
* @param evt the EventObject that we'd like delivered to all
* registered message listerners.
*/
private void fireMessageEvent(EventObject evt)
{
Iterator listeners = null;
synchronized (messageListeners)
{
listeners = new ArrayList(messageListeners).iterator();
}
while (listeners.hasNext())
{
MessageListener listener
= (MessageListener) listeners.next();
if (evt instanceof MessageDeliveredEvent)
{
listener.messageDelivered( (MessageDeliveredEvent) evt);
}
else if (evt instanceof MessageReceivedEvent)
{
listener.messageReceived( (MessageReceivedEvent) evt);
}
else if (evt instanceof MessageDeliveryFailedEvent)
{
listener.messageDeliveryFailed(
(MessageDeliveryFailedEvent) evt);
}
}
}
private class SmackMessageListener
implements PacketListener
{
public void processPacket(Packet packet)
{
if(!(packet instanceof org.jivesoftware.smack.packet.Message))
return;
org.jivesoftware.smack.packet.Message msg =
(org.jivesoftware.smack.packet.Message)packet;
if(msg.getBody() == null)
return;
String fromUserID = StringUtils.parseBareAddress(msg.getFrom());
if(logger.isDebugEnabled())
{
logger.debug("Received from "
+ fromUserID
+ " the message "
+ msg.toXML());
}
Message newMessage = createMessage(msg.getBody());
Contact sourceContact =
opSetPersPresence.findContactByID(fromUserID);
if(msg.getType() == org.jivesoftware.smack.packet.Message.Type.error)
{
logger.info("Message error received from " + fromUserID);
int errorCode = packet.getError().getCode();
int errorResultCode = MessageDeliveryFailedEvent.UNKNOWN_ERROR;
if(errorCode == 503)
{
org.jivesoftware.smackx.packet.MessageEvent msgEvent =
(org.jivesoftware.smackx.packet.MessageEvent)
packet.getExtension("x", "jabber:x:event");
if(msgEvent != null && msgEvent.isOffline())
{
errorResultCode =
MessageDeliveryFailedEvent.OFFLINE_MESSAGES_NOT_SUPPORTED;
}
}
MessageDeliveryFailedEvent ev =
new MessageDeliveryFailedEvent(newMessage,
sourceContact,
errorResultCode,
new Date());
fireMessageEvent(ev);
return;
}
// In the second condition we filter all group chat messages,
// because they are managed by the multi user chat operation set.
if(sourceContact == null)
{
logger.debug("received a message from an unknown contact: "
+ fromUserID);
//create the volatile contact
sourceContact = opSetPersPresence
.createVolatileContact(fromUserID);
}
MessageReceivedEvent msgReceivedEvt
= new MessageReceivedEvent(
newMessage, sourceContact , new Date() );
fireMessageEvent(msgReceivedEvt);
}
}
/**
* Receives incoming KeepAlive Packets
*/
private class KeepalivePacketListener
implements PacketListener
{
public void processPacket(Packet packet)
{
if(packet != null && !(packet instanceof KeepAliveEvent))
return;
KeepAliveEvent keepAliveEvent = (KeepAliveEvent)packet;
if(logger.isDebugEnabled())
{
logger.debug("Received keepAliveEvent from "
+ keepAliveEvent.getFromUserID()
+ " the message : "
+ keepAliveEvent.toXML());
}
receivedKeepAlivePackets.addLast(keepAliveEvent);
}
}
/**
* Task sending packets on intervals.
* The task is runned on specified intervals by the keepAliveTimer
*/
private class KeepAliveSendTask
extends TimerTask
{
public void run()
{
// if we are not registerd do nothing
if(!jabberProvider.isRegistered())
{
logger.trace("provider not registered. "
+"won't send keep alive. acc.id="
+ jabberProvider.getAccountID()
.getAccountUniqueID());
return;
}
KeepAliveEvent keepAliveEvent =
new KeepAliveEvent(jabberProvider.getConnection().getUser());
keepAliveEvent.setSrcOpSetHash(
OperationSetBasicInstantMessagingJabberImpl.this.hashCode());
keepAliveEvent.setSrcProviderHash(jabberProvider.hashCode());
// schedule the check task
keepAliveTimer.schedule(
new KeepAliveCheckTask(), KEEPALIVE_WAIT);
logger.trace(
"send keepalive for acc: "
+ jabberProvider.getAccountID().getAccountUniqueID());
jabberProvider.getConnection().sendPacket(keepAliveEvent);
}
}
/**
* Check if the first received packet in the queue
* is ok and if its not or the queue has no received packets
* the this means there is some network problem, so fire event
*/
private class KeepAliveCheckTask
extends TimerTask
{
public void run()
{
try
{
// check till we find a correct message
// or if NoSuchElementException is thrown
// there is no message
while(!checkFirstPacket());
failedKeepalivePackets = 0;
}
catch (NoSuchElementException ex)
{
logger.error(
"Did not receive last keep alive packet for account "
+ jabberProvider.getAccountID().getAccountUniqueID());
failedKeepalivePackets++;
// if we have 3 keepalive fails then unregister
if(failedKeepalivePackets == 3)
{
logger.error("unregistering.");
// fireUnregisterd();
jabberProvider.reregister();
failedKeepalivePackets = 0;
}
}
}
/**
* Checks whether first packet in queue is ok
* @return boolean
* @throws NoSuchElementException
*/
boolean checkFirstPacket()
throws NoSuchElementException
{
KeepAliveEvent receivedEvent =
(KeepAliveEvent)receivedKeepAlivePackets.removeLast();
if(jabberProvider.hashCode() != receivedEvent.getSrcProviderHash() ||
OperationSetBasicInstantMessagingJabberImpl.this.hashCode() !=
receivedEvent.getSrcOpSetHash() ||
!jabberProvider.getAccountID().getUserID().
equals(receivedEvent.getFromUserID()) )
return false;
else
return true;
}
/**
* Fire Unregistered event
*/
void fireUnregisterd()
{
jabberProvider.fireRegistrationStateChanged(
jabberProvider.getRegistrationState(),
RegistrationState.CONNECTION_FAILED,
RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, null);
opSetPersPresence.fireProviderPresenceStatusChangeEvent(
opSetPersPresence.getPresenceStatus(),
JabberStatusEnum.OFFLINE);
}
}
/**
* Set is keep alive sendin packets to be enabled
* @param keepAliveEnabled boolean
*/
public void setKeepAliveEnabled(boolean keepAliveEnabled)
{
this.keepAliveEnabled = keepAliveEnabled;
}
private class GroupMessagePacketFilter implements PacketFilter
{
public boolean accept(Packet packet)
{
if(!(packet instanceof org.jivesoftware.smack.packet.Message))
return false;
org.jivesoftware.smack.packet.Message msg
= (org.jivesoftware.smack.packet.Message) packet;
if(msg.getType().equals(
org.jivesoftware.smack.packet.Message.Type.groupchat))
return false;
return true;
}
}
}