/** * A straightforward implementation of the basic instant messaging operation set. * * @author Damian Minkov * @author Matthieu Helleringer * @author Alain Knaebel * @author Emil Ivov * @author Hristo Terezov */ public class OperationSetBasicInstantMessagingJabberImpl extends AbstractOperationSetBasicInstantMessaging implements OperationSetMessageCorrection { /** Our class logger */ private static final Logger logger = Logger.getLogger(OperationSetBasicInstantMessagingJabberImpl.class); /** The maximum number of unread threads that we'd be notifying the user of. */ private static final String PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION = "net.java.sip.communicator.impl.protocol.jabber." + "MAX_GMAIL_THREADS_PER_NOTIFICATION"; /** * A table mapping contact addresses to full jids that can be used to target a specific resource * (rather than sending a message to all logged instances of a user). */ private Map<String, StoredThreadID> jids = new Hashtable<String, StoredThreadID>(); /** The most recent full JID used for the contact address. */ private Map<String, String> recentJIDForAddress = new Hashtable<String, String>(); /** * The smackMessageListener instance listens for incoming messages. Keep a reference of it so if * anything goes wrong we don't add two different instances. */ private SmackMessageListener smackMessageListener = null; /** * Contains the complete jid of a specific user and the time that it was last used so that we * could remove it after a certain point. */ public static class StoredThreadID { /** The time that we last sent or received a message from this jid */ long lastUpdatedTime; /** The last chat used, this way we will reuse the thread-id */ String threadID; } /** A prefix helps to make sure that thread ID's are unique across mutliple instances. */ private static String prefix = StringUtils.randomString(5); /** * Keeps track of the current increment, which is appended to the prefix to forum a unique thread * ID. */ private static long id = 0; /** * The number of milliseconds that we preserve threads with no traffic before considering them * dead. */ private static final long JID_INACTIVITY_TIMEOUT = 10 * 60 * 1000; // 10 min. /** * Indicates the time of the last Mailbox report that we received from Google (if this is a Google * server we are talking to). Should be included in all following mailbox queries */ private long lastReceivedMailboxResultTime = -1; /** The provider that created us. */ private final ProtocolProviderServiceJabberImpl jabberProvider; /** * 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; /** The opening BODY HTML TAG: <body> */ private static final String OPEN_BODY_TAG = "<body>"; /** The closing BODY HTML TAG: <body> */ private static final String CLOSE_BODY_TAG = "</body>"; /** The html namespace used as feature XHTMLManager.namespace */ private static final String HTML_NAMESPACE = "http://jabber.org/protocol/xhtml-im"; /** List of filters to be used to filter which messages to handle current Operation Set. */ private List<PacketFilter> packetFilters = new ArrayList<PacketFilter>(); /** Whether carbon is enabled or not. */ private boolean isCarbonEnabled = false; /** * Creates an instance of this operation set. * * @param provider a reference to the <tt>ProtocolProviderServiceImpl</tt> that created us and * that we'll use for retrieving the underlying aim connection. */ OperationSetBasicInstantMessagingJabberImpl(ProtocolProviderServiceJabberImpl provider) { this.jabberProvider = provider; packetFilters.add(new GroupMessagePacketFilter()); packetFilters.add(new PacketTypeFilter(org.jivesoftware.smack.packet.Message.class)); provider.addRegistrationStateChangeListener(new RegistrationStateListener()); ProviderManager man = ProviderManager.getInstance(); MessageCorrectionExtensionProvider extProvider = new MessageCorrectionExtensionProvider(); man.addExtensionProvider( MessageCorrectionExtension.ELEMENT_NAME, MessageCorrectionExtension.NAMESPACE, extProvider); } /** * Create a Message instance with the specified UID, content type and a default encoding. This * method can be useful when message correction is required. One can construct the corrected * message to have the same UID as the message before correction. * * @param messageText the string content of the message. * @param contentType the MIME-type for <tt>content</tt> * @param messageUID the unique identifier of this message. * @return Message the newly created message */ public Message createMessageWithUID(String messageText, String contentType, String messageUID) { return new MessageJabberImpl(messageText, contentType, DEFAULT_MIME_ENCODING, null, messageUID); } /** * Create a Message instance for sending arbitrary MIME-encoding content. * * @param content content value * @param contentType the MIME-type for <tt>content</tt> * @return the newly created message. */ public Message createMessage(String content, String contentType) { return createMessage(content, contentType, DEFAULT_MIME_ENCODING, null); } /** * Create a Message instance for sending arbitrary MIME-encoding content. * * @param content content value * @param contentType the MIME-type for <tt>content</tt> * @param subject the Subject of the message that we'd like to create. * @param encoding the enconding of the message that we will be sending. * @return the newly created message. */ @Override public Message createMessage( String content, String contentType, String encoding, String subject) { return new MessageJabberImpl(content, contentType, encoding, subject); } Message createMessage(String content, String contentType, String messageUID) { return new MessageJabberImpl(content, contentType, DEFAULT_MIME_ENCODING, null, messageUID); } /** * 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 <tt>true</tt> if the protocol supports offline messages and <tt>false</tt> otherwise. */ public boolean isOfflineMessagingSupported() { return true; } /** * Determines wheter the protocol supports the supplied content type * * @param contentType the type we want to check * @return <tt>true</tt> if the protocol supports it and <tt>false</tt> otherwise. */ public boolean isContentTypeSupported(String contentType) { return (contentType.equals(DEFAULT_MIME_TYPE) || contentType.equals(HTML_MIME_TYPE)); } /** * Determines whether the protocol supports the supplied content type for the given contact. * * @param contentType the type we want to check * @param contact contact which is checked for supported contentType * @return <tt>true</tt> if the contact supports it and <tt>false</tt> otherwise. */ @Override public boolean isContentTypeSupported(String contentType, Contact contact) { // by default we support default mime type, for other mimetypes // method must be overriden if (contentType.equals(DEFAULT_MIME_TYPE)) return true; else if (contentType.equals(HTML_MIME_TYPE)) { String toJID = recentJIDForAddress.get(contact.getAddress()); if (toJID == null) toJID = contact.getAddress(); return jabberProvider.isFeatureListSupported(toJID, HTML_NAMESPACE); } return false; } /** * Remove from our <tt>jids</tt> map all entries that have not seen any activity (i.e. neither * outgoing nor incoming messags) for more than JID_INACTIVITY_TIMEOUT. Note that this method is * not synchronous and that it is only meant for use by the {@link #getThreadIDForAddress(String)} * and {@link #putJidForAddress(String, String)} */ private void purgeOldJids() { long currentTime = System.currentTimeMillis(); Iterator<Map.Entry<String, StoredThreadID>> entries = jids.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<String, StoredThreadID> entry = entries.next(); StoredThreadID target = entry.getValue(); if (currentTime - target.lastUpdatedTime > JID_INACTIVITY_TIMEOUT) entries.remove(); } } /** * Returns the last jid that the party with the specified <tt>address</tt> contacted us from or * <tt>null</tt>(or bare jid) if we don't have a jid for the specified <tt>address</tt> yet. The * method would also purge all entries that haven't seen any activity (i.e. no one has tried to * get or remap it) for a delay longer than <tt>JID_INACTIVITY_TIMEOUT</tt>. * * @param jid the <tt>jid</tt> that we'd like to obtain a threadID for. * @return the last jid that the party with the specified <tt>address</tt> contacted us from or * <tt>null</tt> if we don't have a jid for the specified <tt>address</tt> yet. */ String getThreadIDForAddress(String jid) { synchronized (jids) { purgeOldJids(); StoredThreadID ta = jids.get(jid); if (ta == null) return null; ta.lastUpdatedTime = System.currentTimeMillis(); return ta.threadID; } } /** * Maps the specified <tt>address</tt> to <tt>jid</tt>. The point of this method is to allow us to * send all messages destined to the contact with the specified <tt>address</tt> to the * <tt>jid</tt> that they last contacted us from. * * @param threadID the threadID of conversation. * @param jid the jid (i.e. address/resource) that the contact with the specified <tt>address</tt> * last contacted us from. */ private void putJidForAddress(String jid, String threadID) { synchronized (jids) { purgeOldJids(); StoredThreadID ta = jids.get(jid); if (ta == null) { ta = new StoredThreadID(); jids.put(jid, ta); } recentJIDForAddress.put(StringUtils.parseBareAddress(jid), jid); ta.lastUpdatedTime = System.currentTimeMillis(); ta.threadID = threadID; } } /** * Helper function used to send a message to a contact, with the given extensions attached. * * @param to The contact to send the message to. * @param toResource The resource to send the message to or null if no resource has been specified * @param message The message to send. * @param extensions The XMPP extensions that should be attached to the message before sending. * @return The MessageDeliveryEvent that resulted after attempting to send this message, so the * calling function can modify it if needed. */ private MessageDeliveredEvent sendMessage( Contact to, ContactResource toResource, Message message, PacketExtension[] extensions) { if (!(to instanceof ContactJabberImpl)) throw new IllegalArgumentException("The specified contact is not a Jabber contact." + to); assertConnected(); org.jivesoftware.smack.packet.Message msg = new org.jivesoftware.smack.packet.Message(); String toJID = null; if (toResource != null) { if (toResource.equals(ContactResource.BASE_RESOURCE)) { toJID = to.getAddress(); } else toJID = ((ContactResourceJabberImpl) toResource).getFullJid(); } if (toJID == null) { toJID = to.getAddress(); } msg.setPacketID(message.getMessageUID()); msg.setTo(toJID); for (PacketExtension ext : extensions) { msg.addExtension(ext); } if (logger.isTraceEnabled()) logger.trace("Will send a message to:" + toJID + " chat.jid=" + toJID); MessageDeliveredEvent msgDeliveryPendingEvt = new MessageDeliveredEvent(message, to, toResource); MessageDeliveredEvent[] transformedEvents = messageDeliveryPendingTransform(msgDeliveryPendingEvt); if (transformedEvents == null || transformedEvents.length == 0) return null; for (MessageDeliveredEvent event : transformedEvents) { String content = event.getSourceMessage().getContent(); if (message.getContentType().equals(HTML_MIME_TYPE)) { msg.setBody(Html2Text.extractText(content)); // Check if the other user supports XHTML messages // make sure we use our discovery manager as it caches calls if (jabberProvider.isFeatureListSupported(toJID, HTML_NAMESPACE)) { // Add the XHTML text to the message XHTMLManager.addBody(msg, OPEN_BODY_TAG + content + CLOSE_BODY_TAG); } } else { // this is plain text so keep it as it is. msg.setBody(content); } // msg.addExtension(new Version()); if (event.isMessageEncrypted() && isCarbonEnabled) { msg.addExtension(new CarbonPacketExtension.PrivateExtension()); } MessageEventManager.addNotificationsRequests(msg, true, false, false, true); String threadID = getThreadIDForAddress(toJID); if (threadID == null) threadID = nextThreadID(); msg.setThread(threadID); msg.setType(org.jivesoftware.smack.packet.Message.Type.chat); msg.setFrom(jabberProvider.getConnection().getUser()); jabberProvider.getConnection().sendPacket(msg); putJidForAddress(toJID, threadID); } return new MessageDeliveredEvent(message, to, toResource); } /** * Sends the <tt>message</tt> to the destination indicated by the <tt>to</tt> contact. * * @param to the <tt>Contact</tt> to send <tt>message</tt> to * @param message the <tt>Message</tt> to send. * @throws java.lang.IllegalStateException if the underlying stack is not registered and * initialized. * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an instance of ContactImpl. */ public void sendInstantMessage(Contact to, Message message) throws IllegalStateException, IllegalArgumentException { sendInstantMessage(to, null, message); } /** * Sends the <tt>message</tt> to the destination indicated by the <tt>to</tt>. Provides a default * implementation of this method. * * @param to the <tt>Contact</tt> to send <tt>message</tt> to * @param toResource the resource to which the message should be send * @param message the <tt>Message</tt> to send. * @throws java.lang.IllegalStateException if the underlying ICQ stack is not registered and * initialized. * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an instance belonging to the * underlying implementation. */ @Override public void sendInstantMessage(Contact to, ContactResource toResource, Message message) throws IllegalStateException, IllegalArgumentException { MessageDeliveredEvent msgDelivered = sendMessage(to, toResource, message, new PacketExtension[0]); fireMessageEvent(msgDelivered); } /** * Replaces the message with ID <tt>correctedMessageUID</tt> sent to the contact <tt>to</tt> with * the message <tt>message</tt> * * @param to The contact to send the message to. * @param message The new message. * @param correctedMessageUID The ID of the message being replaced. */ public void correctMessage( Contact to, ContactResource resource, Message message, String correctedMessageUID) { PacketExtension[] exts = new PacketExtension[1]; exts[0] = new MessageCorrectionExtension(correctedMessageUID); MessageDeliveredEvent msgDelivered = sendMessage(to, resource, message, exts); msgDelivered.setCorrectedMessageUID(correctedMessageUID); fireMessageEvent(msgDelivered); } /** * 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 (opSetPersPresence == null) { throw new IllegalStateException( "The provider must be signed on the service before" + " being able to communicate."); } else opSetPersPresence.assertConnected(); } /** Our listener that will tell us when we're registered to */ 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 (logger.isDebugEnabled()) logger.debug( "The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState()); if (evt.getNewState() == RegistrationState.REGISTERING) { opSetPersPresence = (OperationSetPersistentPresenceJabberImpl) jabberProvider.getOperationSet(OperationSetPersistentPresence.class); if (smackMessageListener == null) { smackMessageListener = new SmackMessageListener(); } else { // make sure this listener is not already installed in this // connection jabberProvider.getConnection().removePacketListener(smackMessageListener); } jabberProvider .getConnection() .addPacketListener( smackMessageListener, new AndFilter(packetFilters.toArray(new PacketFilter[packetFilters.size()]))); } else if (evt.getNewState() == RegistrationState.REGISTERED) { new Thread( new Runnable() { @Override public void run() { initAdditionalServices(); } }) .start(); } else if (evt.getNewState() == RegistrationState.UNREGISTERED || evt.getNewState() == RegistrationState.CONNECTION_FAILED || evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED) { if (jabberProvider.getConnection() != null) { if (smackMessageListener != null) jabberProvider.getConnection().removePacketListener(smackMessageListener); } smackMessageListener = null; } } } /** Initialize additional services, like gmail notifications and message carbons. */ private void initAdditionalServices() { // subscribe for Google (Gmail or Google Apps) notifications // for new mail messages. boolean enableGmailNotifications = jabberProvider .getAccountID() .getAccountPropertyBoolean("GMAIL_NOTIFICATIONS_ENABLED", false); if (enableGmailNotifications) subscribeForGmailNotifications(); boolean enableCarbon = isCarbonSupported() && !jabberProvider .getAccountID() .getAccountPropertyBoolean(ProtocolProviderFactory.IS_CARBON_DISABLED, false); if (enableCarbon) { enableDisableCarbon(true); } else { isCarbonEnabled = false; } } /** * Sends enable or disable carbon packet to the server. * * @param enable if <tt>true</tt> sends enable packet otherwise sends disable packet. */ private void enableDisableCarbon(final boolean enable) { IQ iq = new IQ() { @Override public String getChildElementXML() { return "<" + (enable ? "enable" : "disable") + " xmlns='urn:xmpp:carbons:2' />"; } }; Packet response = null; try { PacketCollector packetCollector = jabberProvider .getConnection() .createPacketCollector(new PacketIDFilter(iq.getPacketID())); iq.setFrom(jabberProvider.getOurJID()); iq.setType(IQ.Type.SET); jabberProvider.getConnection().sendPacket(iq); response = packetCollector.nextResult(SmackConfiguration.getPacketReplyTimeout()); packetCollector.cancel(); } catch (Exception e) { logger.error("Failed to enable carbon.", e); } isCarbonEnabled = false; if (response == null) { logger.error("Failed to enable carbon. No response is received."); } else if (response.getError() != null) { logger.error("Failed to enable carbon: " + response.getError()); } else if (!(response instanceof IQ) || !((IQ) response).getType().equals(IQ.Type.RESULT)) { logger.error("Failed to enable carbon. The response is not correct."); } else { isCarbonEnabled = true; } } /** * Checks whether the carbon is supported by the server or not. * * @return <tt>true</tt> if carbon is supported by the server and <tt>false</tt> if not. */ private boolean isCarbonSupported() { try { return jabberProvider .getDiscoveryManager() .discoverInfo(jabberProvider.getAccountID().getService()) .containsFeature(CarbonPacketExtension.NAMESPACE); } catch (XMPPException e) { logger.warn("Failed to retrieve carbon support." + e.getMessage()); } return false; } /** The listener that we use in order to handle incoming messages. */ @SuppressWarnings("unchecked") private class SmackMessageListener implements PacketListener { /** * Handles incoming messages and dispatches whatever events are necessary. * * @param packet the packet that we need to handle (if it is a message). */ 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; boolean isForwardedSentMessage = false; if (msg.getBody() == null) { CarbonPacketExtension carbonExt = (CarbonPacketExtension) msg.getExtension(CarbonPacketExtension.NAMESPACE); if (carbonExt == null) return; isForwardedSentMessage = (carbonExt.getElementName() == CarbonPacketExtension.SENT_ELEMENT_NAME); List<ForwardedPacketExtension> extensions = carbonExt.getChildExtensionsOfType(ForwardedPacketExtension.class); if (extensions.isEmpty()) return; ForwardedPacketExtension forwardedExt = extensions.get(0); msg = forwardedExt.getMessage(); if (msg == null || msg.getBody() == null) return; } Object multiChatExtension = msg.getExtension("x", "http://jabber.org/protocol/muc#user"); // its not for us if (multiChatExtension != null) return; String userFullId = isForwardedSentMessage ? msg.getTo() : msg.getFrom(); String userBareID = StringUtils.parseBareAddress(userFullId); boolean isPrivateMessaging = false; ChatRoom privateContactRoom = null; OperationSetMultiUserChatJabberImpl mucOpSet = (OperationSetMultiUserChatJabberImpl) jabberProvider.getOperationSet(OperationSetMultiUserChat.class); if (mucOpSet != null) privateContactRoom = mucOpSet.getChatRoom(userBareID); if (privateContactRoom != null) { isPrivateMessaging = true; } if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) logger.debug("Received from " + userBareID + " the message " + msg.toXML()); } Message newMessage = createMessage(msg.getBody(), DEFAULT_MIME_TYPE, msg.getPacketID()); // check if the message is available in xhtml PacketExtension ext = msg.getExtension("http://jabber.org/protocol/xhtml-im"); if (ext != null) { XHTMLExtension xhtmlExt = (XHTMLExtension) ext; // parse all bodies Iterator<String> bodies = xhtmlExt.getBodies(); StringBuffer messageBuff = new StringBuffer(); while (bodies.hasNext()) { String body = bodies.next(); messageBuff.append(body); } if (messageBuff.length() > 0) { // we remove body tags around message cause their // end body tag is breaking // the visualization as html in the UI String receivedMessage = messageBuff .toString() // removes body start tag .replaceAll("\\<[bB][oO][dD][yY].*?>", "") // removes body end tag .replaceAll("\\</[bB][oO][dD][yY].*?>", ""); // for some reason ' is not rendered correctly // from our ui, lets use its equivalent. Other // similar chars(< > & ") seem ok. receivedMessage = receivedMessage.replaceAll("'", "'"); newMessage = createMessage(receivedMessage, HTML_MIME_TYPE, msg.getPacketID()); } } PacketExtension correctionExtension = msg.getExtension(MessageCorrectionExtension.NAMESPACE); String correctedMessageUID = null; if (correctionExtension != null) { correctedMessageUID = ((MessageCorrectionExtension) correctionExtension).getCorrectedMessageUID(); } Contact sourceContact = opSetPersPresence.findContactByID((isPrivateMessaging ? userFullId : userBareID)); if (msg.getType() == org.jivesoftware.smack.packet.Message.Type.error) { // error which is multichat and we don't know about the contact // is a muc message error which is missing muc extension // and is coming from the room, when we try to send message to // room which was deleted or offline on the server if (isPrivateMessaging && sourceContact == null) { if (privateContactRoom != null) { XMPPError error = packet.getError(); int errorResultCode = ChatRoomMessageDeliveryFailedEvent.UNKNOWN_ERROR; if (error != null && error.getCode() == 403) { errorResultCode = ChatRoomMessageDeliveryFailedEvent.FORBIDDEN; } String errorReason = error.getMessage(); ChatRoomMessageDeliveryFailedEvent evt = new ChatRoomMessageDeliveryFailedEvent( privateContactRoom, null, errorResultCode, errorReason, new Date(), newMessage); ((ChatRoomJabberImpl) privateContactRoom).fireMessageEvent(evt); } return; } if (logger.isInfoEnabled()) logger.info("Message error received from " + userBareID); int errorResultCode = MessageDeliveryFailedEvent.UNKNOWN_ERROR; if (packet.getError() != null) { int errorCode = packet.getError().getCode(); 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; } } } if (sourceContact == null) { sourceContact = opSetPersPresence.createVolatileContact(userFullId, isPrivateMessaging); } MessageDeliveryFailedEvent ev = new MessageDeliveryFailedEvent( newMessage, sourceContact, correctedMessageUID, errorResultCode); // ev = messageDeliveryFailedTransform(ev); if (ev != null) fireMessageEvent(ev); return; } putJidForAddress(userFullId, msg.getThread()); // In the second condition we filter all group chat messages, // because they are managed by the multi user chat operation set. if (sourceContact == null) { if (logger.isDebugEnabled()) logger.debug("received a message from an unknown contact: " + userBareID); // create the volatile contact sourceContact = opSetPersPresence.createVolatileContact(userFullId, isPrivateMessaging); } Date timestamp = new Date(); // Check for XEP-0091 timestamp (deprecated) PacketExtension delay = msg.getExtension("x", "jabber:x:delay"); if (delay != null && delay instanceof DelayInformation) { timestamp = ((DelayInformation) delay).getStamp(); } // check for XEP-0203 timestamp delay = msg.getExtension("delay", "urn:xmpp:delay"); if (delay != null && delay instanceof DelayInfo) { timestamp = ((DelayInfo) delay).getStamp(); } ContactResource resource = ((ContactJabberImpl) sourceContact).getResourceFromJid(userFullId); EventObject msgEvt = null; if (!isForwardedSentMessage) msgEvt = new MessageReceivedEvent( newMessage, sourceContact, resource, timestamp, correctedMessageUID, isPrivateMessaging, privateContactRoom); else msgEvt = new MessageDeliveredEvent(newMessage, sourceContact, timestamp); // msgReceivedEvt = messageReceivedTransform(msgReceivedEvt); if (msgEvt != null) fireMessageEvent(msgEvt); } } /** A filter that prevents this operation set from handling multi user chat messages. */ private static class GroupMessagePacketFilter implements PacketFilter { /** * Returns <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and false otherwise. * * @param packet the packet that we need to check. * @return <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and false otherwise. */ 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; return !msg.getType().equals(org.jivesoftware.smack.packet.Message.Type.groupchat); } } /** * Subscribes this provider as interested in receiving notifications for new mail messages from * Google mail services such as Gmail or Google Apps. */ private void subscribeForGmailNotifications() { // first check support for the notification service String accountIDService = jabberProvider.getAccountID().getService(); boolean notificationsAreSupported = jabberProvider.isFeatureSupported(accountIDService, NewMailNotificationIQ.NAMESPACE); if (!notificationsAreSupported) { if (logger.isDebugEnabled()) logger.debug( accountIDService + " does not seem to provide a Gmail notification " + " service so we won't be trying to subscribe for it"); return; } if (logger.isDebugEnabled()) logger.debug( accountIDService + " seems to provide a Gmail notification " + " service so we will try to subscribe for it"); ProviderManager providerManager = ProviderManager.getInstance(); providerManager.addIQProvider( MailboxIQ.ELEMENT_NAME, MailboxIQ.NAMESPACE, new MailboxIQProvider()); providerManager.addIQProvider( NewMailNotificationIQ.ELEMENT_NAME, NewMailNotificationIQ.NAMESPACE, new NewMailNotificationProvider()); Connection connection = jabberProvider.getConnection(); connection.addPacketListener(new MailboxIQListener(), new PacketTypeFilter(MailboxIQ.class)); connection.addPacketListener( new NewMailNotificationListener(), new PacketTypeFilter(NewMailNotificationIQ.class)); if (opSetPersPresence.getCurrentStatusMessage().equals(JabberStatusEnum.OFFLINE)) return; // create a query with -1 values for newer-than-tid and // newer-than-time attributes MailboxQueryIQ mailboxQuery = new MailboxQueryIQ(); if (logger.isTraceEnabled()) logger.trace( "sending mailNotification for acc: " + jabberProvider.getAccountID().getAccountUniqueID()); jabberProvider.getConnection().sendPacket(mailboxQuery); } /** * Creates an html description of the specified mailbox. * * @param mailboxIQ the mailboxIQ that we are to describe. * @return an html description of <tt>mailboxIQ</tt> */ private String createMailboxDescription(MailboxIQ mailboxIQ) { int threadCount = mailboxIQ.getThreadCount(); String resourceHeaderKey = threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_HEADER" : "service.gui.NEW_GMAIL_HEADER"; String resourceFooterKey = threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_FOOTER" : "service.gui.NEW_GMAIL_FOOTER"; // FIXME Escape HTML! String newMailHeader = JabberActivator.getResources() .getI18NString( resourceHeaderKey, new String[] { jabberProvider.getAccountID().getService(), // {0} - service name mailboxIQ.getUrl(), // {1} - inbox URI Integer.toString(threadCount) // {2} - thread count }); StringBuilder message = new StringBuilder(newMailHeader); // we now start an html table for the threads. message.append("<table width=100% cellpadding=2 cellspacing=0 "); message.append("border=0 bgcolor=#e8eef7>"); Iterator<MailThreadInfo> threads = mailboxIQ.threads(); String maxThreadsStr = (String) JabberActivator.getConfigurationService() .getProperty(PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION); int maxThreads = 5; try { if (maxThreadsStr != null) maxThreads = Integer.parseInt(maxThreadsStr); } catch (NumberFormatException e) { if (logger.isDebugEnabled()) logger.debug("Failed to parse max threads count: " + maxThreads + ". Going for default."); } // print a maximum of MAX_THREADS for (int i = 0; i < maxThreads && threads.hasNext(); i++) { message.append(threads.next().createHtmlDescription()); } message.append("</table><br/>"); if (threadCount > maxThreads) { String messageFooter = JabberActivator.getResources() .getI18NString( resourceFooterKey, new String[] { mailboxIQ.getUrl(), // {0} - inbox URI Integer.toString(threadCount - maxThreads) // {1} - thread count }); message.append(messageFooter); } return message.toString(); } public String getRecentJIDForAddress(String address) { return recentJIDForAddress.get(address); } /** Receives incoming MailNotification Packets */ private class MailboxIQListener implements PacketListener { /** * Handles incoming <tt>MailboxIQ</tt> packets. * * @param packet the IQ that we need to handle in case it is a <tt>MailboxIQ</tt>. */ public void processPacket(Packet packet) { if (packet != null && !(packet instanceof MailboxIQ)) return; MailboxIQ mailboxIQ = (MailboxIQ) packet; if (mailboxIQ.getTotalMatched() < 1) return; // Get a reference to a dummy volatile contact Contact sourceContact = opSetPersPresence.findContactByID(jabberProvider.getAccountID().getService()); if (sourceContact == null) sourceContact = opSetPersPresence.createVolatileContact(jabberProvider.getAccountID().getService()); lastReceivedMailboxResultTime = mailboxIQ.getResultTime(); String newMail = createMailboxDescription(mailboxIQ); Message newMailMessage = new MessageJabberImpl(newMail, HTML_MIME_TYPE, DEFAULT_MIME_ENCODING, null); MessageReceivedEvent msgReceivedEvt = new MessageReceivedEvent( newMailMessage, sourceContact, new Date(), MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED); fireMessageEvent(msgReceivedEvt); } } /** Receives incoming NewMailNotification Packets. */ private class NewMailNotificationListener implements PacketListener { /** * Handles incoming <tt>NewMailNotificationIQ</tt> packets. * * @param packet the IQ that we need to handle in case it is a <tt>NewMailNotificationIQ</tt>. */ public void processPacket(Packet packet) { if (packet != null && !(packet instanceof NewMailNotificationIQ)) return; // check whether we are still enabled. boolean enableGmailNotifications = jabberProvider .getAccountID() .getAccountPropertyBoolean("GMAIL_NOTIFICATIONS_ENABLED", false); if (!enableGmailNotifications) return; if (opSetPersPresence.getCurrentStatusMessage().equals(JabberStatusEnum.OFFLINE)) return; MailboxQueryIQ mailboxQueryIQ = new MailboxQueryIQ(); if (lastReceivedMailboxResultTime != -1) mailboxQueryIQ.setNewerThanTime(lastReceivedMailboxResultTime); if (logger.isTraceEnabled()) logger.trace( "send mailNotification for acc: " + jabberProvider.getAccountID().getAccountUniqueID()); jabberProvider.getConnection().sendPacket(mailboxQueryIQ); } } /** * Returns the inactivity timeout in milliseconds. * * @return The inactivity timeout in milliseconds. Or -1 if undefined */ public long getInactivityTimeout() { return JID_INACTIVITY_TIMEOUT; } /** * Adds additional filters for incoming messages. To be able to skip some messages. * * @param filter to add */ public void addMessageFilters(PacketFilter filter) { this.packetFilters.add(filter); } /** * Returns the next unique thread id. Each thread id made up of a short alphanumeric prefix along * with a unique numeric value. * * @return the next thread id. */ public static synchronized String nextThreadID() { return prefix + Long.toString(id++); } }
/** * The Jabber implementation of the <tt>OperationSetFileTransfer</tt> interface. * * @author Gregory Bande * @author Nicolas Riegel * @author Yana Stamcheva */ public class OperationSetFileTransferJabberImpl implements OperationSetFileTransfer { /** The logger for this class. */ private static final Logger logger = Logger.getLogger(OperationSetFileTransferJabberImpl.class); /** The provider that created us. */ private final ProtocolProviderServiceJabberImpl jabberProvider; /** An active instance of the opSetPersPresence operation set. */ private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null; /** The Jabber file transfer manager. */ private FileTransferManager manager = null; /** The Jabber file transfer listener. */ private FileTransferRequestListener fileTransferRequestListener; /** A list of listeners registered for file transfer events. */ private Vector<FileTransferListener> fileTransferListeners = new Vector<FileTransferListener>(); // Register file transfer features on every established connection // to make sure we register them before creating our // ServiceDiscoveryManager static { Connection.addConnectionCreationListener( new ConnectionCreationListener() { public void connectionCreated(Connection connection) { FileTransferNegotiator.getInstanceFor(connection); } }); } /** * Constructor * * @param provider is the provider that created us */ public OperationSetFileTransferJabberImpl(ProtocolProviderServiceJabberImpl provider) { this.jabberProvider = provider; provider.addRegistrationStateChangeListener(new RegistrationStateListener()); // use only ibb for file transfers FileTransferNegotiator.IBB_ONLY = true; } /** * Sends a file transfer request to the given <tt>toContact</tt>. * * @return the transfer object * @param toContact the contact that should receive the file * @param file file to send */ public FileTransfer sendFile(Contact toContact, File file) throws IllegalStateException, IllegalArgumentException, OperationNotSupportedException { return sendFile(toContact, file, null); } /** * Sends a file transfer request to the given <tt>toContact</tt>. * * @return the transfer object * @param toContact the contact that should receive the file * @param file file to send * @param gw special gateway to be used for receiver if its jid misses the domain part */ FileTransfer sendFile(Contact toContact, File file, String gw) throws IllegalStateException, IllegalArgumentException, OperationNotSupportedException { OutgoingFileTransferJabberImpl outgoingTransfer = null; try { assertConnected(); if (file.length() > getMaximumFileLength()) throw new IllegalArgumentException("File length exceeds the allowed one for this protocol"); String fullJid = null; // Find the jid of the contact which support file transfer // and is with highest priority if more than one found // if we have equals priorities // choose the one that is more available OperationSetMultiUserChat mucOpSet = jabberProvider.getOperationSet(OperationSetMultiUserChat.class); if (mucOpSet != null && mucOpSet.isPrivateMessagingContact(toContact.getAddress())) { fullJid = toContact.getAddress(); } else { Iterator<Presence> iter = jabberProvider.getConnection().getRoster().getPresences(toContact.getAddress()); int bestPriority = -1; PresenceStatus jabberStatus = null; while (iter.hasNext()) { Presence presence = iter.next(); if (jabberProvider.isFeatureListSupported( presence.getFrom(), new String[] { "http://jabber.org/protocol/si", "http://jabber.org/protocol/si/profile/file-transfer" })) { int priority = (presence.getPriority() == Integer.MIN_VALUE) ? 0 : presence.getPriority(); if (priority > bestPriority) { bestPriority = priority; fullJid = presence.getFrom(); jabberStatus = OperationSetPersistentPresenceJabberImpl.jabberStatusToPresenceStatus( presence, jabberProvider); } else if (priority == bestPriority && jabberStatus != null) { PresenceStatus tempStatus = OperationSetPersistentPresenceJabberImpl.jabberStatusToPresenceStatus( presence, jabberProvider); if (tempStatus.compareTo(jabberStatus) > 0) { fullJid = presence.getFrom(); jabberStatus = tempStatus; } } } } } // First we check if file transfer is at all supported for this // contact. if (fullJid == null) { throw new OperationNotSupportedException( "Contact client or server does not support file transfers."); } if (gw != null && !fullJid.contains("@") && !fullJid.endsWith(gw)) { fullJid = fullJid + "@" + gw; } OutgoingFileTransfer transfer = manager.createOutgoingFileTransfer(fullJid); outgoingTransfer = new OutgoingFileTransferJabberImpl(toContact, file, transfer, jabberProvider); // Notify all interested listeners that a file transfer has been // created. FileTransferCreatedEvent event = new FileTransferCreatedEvent(outgoingTransfer, new Date()); fireFileTransferCreated(event); // Send the file through the Jabber file transfer. transfer.sendFile(file, "Sending file"); // Start the status and progress thread. new FileTransferProgressThread(transfer, outgoingTransfer).start(); } catch (XMPPException e) { logger.error("Failed to send file.", e); } return outgoingTransfer; } /** * Sends a file transfer request to the given <tt>toContact</tt> by specifying the local and * remote file path and the <tt>fromContact</tt>, sending the file. * * @return the transfer object * @param toContact the contact that should receive the file * @param fromContact the contact sending the file * @param remotePath the remote file path * @param localPath the local file path */ public FileTransfer sendFile( Contact toContact, Contact fromContact, String remotePath, String localPath) throws IllegalStateException, IllegalArgumentException, OperationNotSupportedException { return this.sendFile(toContact, new File(localPath)); } /** * Adds the given <tt>FileTransferListener</tt> that would listen for file transfer requests and * created file transfers. * * @param listener the <tt>FileTransferListener</tt> to add */ public void addFileTransferListener(FileTransferListener listener) { synchronized (fileTransferListeners) { if (!fileTransferListeners.contains(listener)) { this.fileTransferListeners.add(listener); } } } /** * Removes the given <tt>FileTransferListener</tt> that listens for file transfer requests and * created file transfers. * * @param listener the <tt>FileTransferListener</tt> to remove */ public void removeFileTransferListener(FileTransferListener listener) { synchronized (fileTransferListeners) { this.fileTransferListeners.remove(listener); } } /** * 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 send a file."); else if (!jabberProvider.isRegistered()) { // if we are not registered but the current status is online // change the current status if (opSetPersPresence.getPresenceStatus().isOnline()) { opSetPersPresence.fireProviderStatusChangeEvent( opSetPersPresence.getPresenceStatus(), jabberProvider.getJabberStatusEnum().getStatus(JabberStatusEnum.OFFLINE)); } throw new IllegalStateException( "The provider must be signed on the service before " + "being able to send a file."); } } /** * Returns the maximum file length supported by the protocol in bytes. Supports up to 2GB. * * @return the file length that is supported. */ public long getMaximumFileLength() { return 2147483648l; // = 2048*1024*1024; } /** Our listener that will tell us when we're registered to */ 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 (logger.isDebugEnabled()) logger.debug( "The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState()); if (evt.getNewState() == RegistrationState.REGISTERED) { opSetPersPresence = (OperationSetPersistentPresenceJabberImpl) jabberProvider.getOperationSet(OperationSetPersistentPresence.class); // Create the Jabber FileTransferManager. manager = new FileTransferManager(jabberProvider.getConnection()); fileTransferRequestListener = new FileTransferRequestListener(); ProviderManager.getInstance() .addIQProvider(FileElement.ELEMENT_NAME, FileElement.NAMESPACE, new FileElement()); ProviderManager.getInstance() .addIQProvider(ThumbnailIQ.ELEMENT_NAME, ThumbnailIQ.NAMESPACE, new ThumbnailIQ()); jabberProvider .getConnection() .addPacketListener( fileTransferRequestListener, new AndFilter( new PacketTypeFilter(StreamInitiation.class), new IQTypeFilter(IQ.Type.SET))); } else if (evt.getNewState() == RegistrationState.UNREGISTERED) { if (fileTransferRequestListener != null && jabberProvider.getConnection() != null) { jabberProvider.getConnection().removePacketListener(fileTransferRequestListener); } ProviderManager providerManager = ProviderManager.getInstance(); if (providerManager != null) { ProviderManager.getInstance() .removeIQProvider(FileElement.ELEMENT_NAME, FileElement.NAMESPACE); ProviderManager.getInstance() .removeIQProvider(ThumbnailIQ.ELEMENT_NAME, ThumbnailIQ.NAMESPACE); } fileTransferRequestListener = null; manager = null; } } } /** Listener for Jabber incoming file transfer requests. */ private class FileTransferRequestListener implements PacketListener { /** * Listens for file transfer packets. * * @param packet packet to be processed */ public void processPacket(Packet packet) { if (!(packet instanceof StreamInitiation)) return; if (logger.isDebugEnabled()) logger.debug("Incoming Jabber file transfer request."); StreamInitiation streamInitiation = (StreamInitiation) packet; FileTransferRequest jabberRequest = new FileTransferRequest(manager, streamInitiation); // Create a global incoming file transfer request. IncomingFileTransferRequestJabberImpl incomingFileTransferRequest = new IncomingFileTransferRequestJabberImpl( jabberProvider, OperationSetFileTransferJabberImpl.this, jabberRequest); // Send a thumbnail request if a thumbnail is advertised in the // streamInitiation packet. org.jivesoftware.smackx.packet.StreamInitiation.File file = streamInitiation.getFile(); boolean isThumbnailedFile = false; if (file instanceof FileElement) { ThumbnailElement thumbnailElement = ((FileElement) file).getThumbnailElement(); if (thumbnailElement != null) { isThumbnailedFile = true; incomingFileTransferRequest.createThumbnailListeners(thumbnailElement.getCid()); ThumbnailIQ thumbnailRequest = new ThumbnailIQ( streamInitiation.getTo(), streamInitiation.getFrom(), thumbnailElement.getCid(), IQ.Type.GET); if (logger.isDebugEnabled()) logger.debug("Sending thumbnail request:" + thumbnailRequest.toXML()); jabberProvider.getConnection().sendPacket(thumbnailRequest); } } if (!isThumbnailedFile) { // Create an event associated to this global request. FileTransferRequestEvent fileTransferRequestEvent = new FileTransferRequestEvent( OperationSetFileTransferJabberImpl.this, incomingFileTransferRequest, new Date()); // Notify the global listener that a request has arrived. fireFileTransferRequest(fileTransferRequestEvent); } } } /** * Delivers the specified event to all registered file transfer listeners. * * @param event the <tt>EventObject</tt> that we'd like delivered to all registered file transfer * listeners. */ void fireFileTransferRequest(FileTransferRequestEvent event) { Iterator<FileTransferListener> listeners = null; synchronized (fileTransferListeners) { listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator(); } while (listeners.hasNext()) { FileTransferListener listener = listeners.next(); listener.fileTransferRequestReceived(event); } } /** * Delivers the specified event to all registered file transfer listeners. * * @param event the <tt>EventObject</tt> that we'd like delivered to all registered file transfer * listeners. */ void fireFileTransferRequestRejected(FileTransferRequestEvent event) { Iterator<FileTransferListener> listeners = null; synchronized (fileTransferListeners) { listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator(); } while (listeners.hasNext()) { FileTransferListener listener = listeners.next(); listener.fileTransferRequestRejected(event); } } /** * Delivers the file transfer to all registered listeners. * * @param event the <tt>FileTransferEvent</tt> that we'd like delivered to all registered file * transfer listeners. */ void fireFileTransferCreated(FileTransferCreatedEvent event) { Iterator<FileTransferListener> listeners = null; synchronized (fileTransferListeners) { listeners = new ArrayList<FileTransferListener>(fileTransferListeners).iterator(); } while (listeners.hasNext()) { FileTransferListener listener = listeners.next(); listener.fileTransferCreated(event); } } /** Updates file transfer progress and status while sending or receiving a file. */ protected static class FileTransferProgressThread extends Thread { private final org.jivesoftware.smackx.filetransfer.FileTransfer jabberTransfer; private final AbstractFileTransfer fileTransfer; private long initialFileSize; public FileTransferProgressThread( org.jivesoftware.smackx.filetransfer.FileTransfer jabberTransfer, AbstractFileTransfer transfer, long initialFileSize) { this.jabberTransfer = jabberTransfer; this.fileTransfer = transfer; this.initialFileSize = initialFileSize; } public FileTransferProgressThread( org.jivesoftware.smackx.filetransfer.FileTransfer jabberTransfer, AbstractFileTransfer transfer) { this.jabberTransfer = jabberTransfer; this.fileTransfer = transfer; } /** Thread entry point. */ @Override public void run() { int status; long progress; String statusReason = ""; while (true) { try { Thread.sleep(10); status = parseJabberStatus(jabberTransfer.getStatus()); progress = fileTransfer.getTransferedBytes(); if (status == FileTransferStatusChangeEvent.FAILED || status == FileTransferStatusChangeEvent.COMPLETED || status == FileTransferStatusChangeEvent.CANCELED || status == FileTransferStatusChangeEvent.REFUSED) { if (fileTransfer instanceof OutgoingFileTransferJabberImpl) { ((OutgoingFileTransferJabberImpl) fileTransfer).removeThumbnailRequestListener(); } // sometimes a filetransfer can be preparing // and than completed : // transfered in one iteration of current thread // so it won't go through intermediate state - inProgress // make sure this won't happen if (status == FileTransferStatusChangeEvent.COMPLETED && fileTransfer.getStatus() == FileTransferStatusChangeEvent.PREPARING) { fileTransfer.fireStatusChangeEvent( FileTransferStatusChangeEvent.IN_PROGRESS, "Status changed"); fileTransfer.fireProgressChangeEvent(System.currentTimeMillis(), progress); } break; } fileTransfer.fireStatusChangeEvent(status, "Status changed"); fileTransfer.fireProgressChangeEvent(System.currentTimeMillis(), progress); } catch (InterruptedException e) { if (logger.isDebugEnabled()) logger.debug("Unable to sleep thread.", e); } } if (jabberTransfer.getError() != null) { logger.error( "An error occured while transfering file: " + jabberTransfer.getError().getMessage()); } if (jabberTransfer.getException() != null) { logger.error( "An exception occured while transfering file: ", jabberTransfer.getException()); if (jabberTransfer.getException() instanceof XMPPException) { XMPPError error = ((XMPPException) jabberTransfer.getException()).getXMPPError(); if (error != null) if (error.getCode() == 406 || error.getCode() == 403) status = FileTransferStatusChangeEvent.REFUSED; } statusReason = jabberTransfer.getException().getMessage(); } if (initialFileSize > 0 && status == FileTransferStatusChangeEvent.COMPLETED && fileTransfer.getTransferedBytes() < initialFileSize) { status = FileTransferStatusChangeEvent.CANCELED; } fileTransfer.fireStatusChangeEvent(status, statusReason); fileTransfer.fireProgressChangeEvent(System.currentTimeMillis(), progress); } } /** * Parses the given Jabber status to a <tt>FileTransfer</tt> interface status. * * @param jabberStatus the Jabber status to parse * @return the parsed status */ private static int parseJabberStatus(Status jabberStatus) { if (jabberStatus.equals(Status.complete)) return FileTransferStatusChangeEvent.COMPLETED; else if (jabberStatus.equals(Status.cancelled)) return FileTransferStatusChangeEvent.CANCELED; else if (jabberStatus.equals(Status.in_progress) || jabberStatus.equals(Status.negotiated)) return FileTransferStatusChangeEvent.IN_PROGRESS; else if (jabberStatus.equals(Status.error)) return FileTransferStatusChangeEvent.FAILED; else if (jabberStatus.equals(Status.refused)) return FileTransferStatusChangeEvent.REFUSED; else if (jabberStatus.equals(Status.negotiating_transfer) || jabberStatus.equals(Status.negotiating_stream)) return FileTransferStatusChangeEvent.PREPARING; else // FileTransfer.Status.initial return FileTransferStatusChangeEvent.WAITING; } }
/** * The Jabber implementation of the service.protocol.Contact interface. * * @author Damian Minkov */ public class ContactJabberImpl implements Contact { private static final Logger logger = Logger.getLogger(ContactJabberImpl.class); private RosterEntry rosterEntry = null; private boolean isLocal = false; private byte[] image = null; private PresenceStatus status = JabberStatusEnum.OFFLINE; private ServerStoredContactListJabberImpl ssclCallback = null; private boolean isPersistent = false; private boolean isResolved = false; private String tempId = null; private String statusMessage = null; /** * Creates an JabberContactImpl * * @param rosterEntry the RosterEntry object that we will be encapsulating. * @param ssclCallback a reference to the ServerStoredContactListImpl instance that created us. * @param isPersistent determines whether this contact is persistent or not. * @param isResolved specifies whether the contact has been resolved against the server contact * list */ ContactJabberImpl( RosterEntry rosterEntry, ServerStoredContactListJabberImpl ssclCallback, boolean isPersistent, boolean isResolved) { this.rosterEntry = rosterEntry; this.isLocal = isLocal; this.ssclCallback = ssclCallback; this.isPersistent = isPersistent; this.isResolved = isResolved; } ContactJabberImpl( String id, ServerStoredContactListJabberImpl ssclCallback, boolean isPersistent) { this.tempId = id; this.isLocal = isLocal; this.ssclCallback = ssclCallback; this.isPersistent = isPersistent; this.isResolved = false; } /** * Returns the Jabber Userid of this contact * * @return the Jabber Userid of this contact */ public String getAddress() { if (isResolved) return rosterEntry.getUser(); else return tempId; } /** * Determines whether or not this Contact instance represents the user used by this protocol * provider to connect to the service. * * @return true if this Contact represents us (the local user) and false otherwise. */ public boolean isLocal() { return isLocal; } /** Returns the image of the contact or null if absent */ public byte[] getImage() { if (image == null) ssclCallback.addContactForImageUpdate(this); return image; } /** Set the image of the contact */ void setImage(byte[] imgBytes) { this.image = imgBytes; } /** * Returns a hashCode for this contact. The returned hashcode is actually that of the Contact's * Address * * @return the hashcode of this Contact */ public int hashCode() { return getAddress().hashCode(); } /** * Indicates whether some other object is "equal to" this one. * * <p> * * @param obj the reference object with which to compare. * @return <tt>true</tt> if this object is the same as the obj argument; <tt>false</tt> otherwise. */ public boolean equals(Object obj) { if (obj == null || !(obj instanceof ContactJabberImpl) || !(((ContactJabberImpl) obj).getAddress().equals(getAddress()) && ((ContactJabberImpl) obj).getProtocolProvider() == getProtocolProvider())) return false; return true; } /** * Returns a string representation of this contact, containing most of its representative details. * * @return a string representation of this contact. */ public String toString() { StringBuffer buff = new StringBuffer("JabberContact[ id="); buff.append(getAddress()) .append(", isPersistent=") .append(isPersistent) .append(", isResolved=") .append(isResolved) .append("]"); return buff.toString(); } /** * Sets the status that this contact is currently in. The method is to only be called as a result * of a status update received from the server. * * @param status the JabberStatusEnum that this contact is currently in. */ void updatePresenceStatus(PresenceStatus status) { this.status = status; } /** * Returns the status of the contact as per the last status update we've received for it. Note * that this method is not to perform any network operations and will simply return the status * received in the last status update message. If you want a reliable way of retrieving someone's * status, you should use the <tt>queryContactStatus()</tt> method in * <tt>OperationSetPresence</tt>. * * @return the PresenceStatus that we've received in the last status update pertaining to this * contact. */ public PresenceStatus getPresenceStatus() { return status; } /** * Returns a String that could be used by any user interacting modules for referring to this * contact. An alias is not necessarily unique but is often more human readable than an address * (or id). * * @return a String that can be used for referring to this contact when interacting with the user. */ public String getDisplayName() { if (isResolved) { String name = rosterEntry.getName(); if (name == null) name = getAddress(); return name; } else return tempId; } /** * Returns a reference to the contact group that this contact is currently a child of or null if * the underlying protocol does not suppord persistent presence. * * @return a reference to the contact group that this contact is currently a child of or null if * the underlying protocol does not suppord persistent presence. */ public ContactGroup getParentContactGroup() { return ssclCallback.findContactGroup(this); } /** * Returns a reference to the protocol provider that created the contact. * * @return a refererence to an instance of the ProtocolProviderService */ public ProtocolProviderService getProtocolProvider() { return ssclCallback.getParentProvider(); } /** * Determines whether or not this contact is being stored by the server. Non persistent contacts * are common in the case of simple, non-persistent presence operation sets. They could however * also be seen in persistent presence operation sets when for example we have received an event * from someone not on our contact list. Non persistent contacts are volatile even when coming * from a persistent presence op. set. They would only exist until the application is closed and * will not be there next time it is loaded. * * @return true if the contact is persistent and false otherwise. */ public boolean isPersistent() { return isPersistent; } /** * Specifies whether this contact is to be considered persistent or not. The method is to be used * _only_ when a non-persistent contact has been added to the contact list and its encapsulated * VolatileBuddy has been repalced with a standard buddy. * * @param persistent true if the buddy is to be considered persistent and false for volatile. */ void setPersistent(boolean persistent) { this.isPersistent = persistent; } /** * Resolve this contact against the given entry * * @param entry the server stored entry */ void setResolved(RosterEntry entry) { if (isResolved) return; this.isResolved = true; rosterEntry = entry; } /** * Returns the persistent data * * @return the persistent data */ public String getPersistentData() { return null; } /** * Determines whether or not this contact has been resolved against the server. Unresolved * contacts are used when initially loading a contact list that has been stored in a local file * until the presence operation set has managed to retrieve all the contact list from the server * and has properly mapped contacts to their on-line buddies. * * @return true if the contact has been resolved (mapped against a buddy) and false otherwise. */ public boolean isResolved() { return isResolved; } public void setPersistentData(String persistentData) {} /** * Get source entry * * @return RosterEntry */ RosterEntry getSourceEntry() { return rosterEntry; } /** * Return the current status message of this contact. * * @return the current status message */ public String getStatusMessage() { return statusMessage; } /** * Sets the current status message for this contact * * @param statusMessage the message */ protected void setStatusMessage(String statusMessage) { this.statusMessage = statusMessage; } }