/** * Computes and returns the hash of the specified <tt>capsString</tt> using the specified * <tt>hashAlgorithm</tt>. * * @param hashAlgorithm the name of the algorithm to be used to generate the hash * @param capsString the capabilities string that we'd like to compute a hash for. * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or * <tt>null</tt> if generating the hash has failed */ private static String capsToHash(String hashAlgorithm, String capsString) { try { MessageDigest md = MessageDigest.getInstance(hashAlgorithm); byte[] digest = md.digest(capsString.getBytes()); return Base64.encodeBytes(digest); } catch (NoSuchAlgorithmException nsae) { logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm); return null; } }
/** * 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; }
/** * 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; } }
/** * 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); }
/** 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); }
/** * 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; } }
/** * 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; }
/** * 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(); }
/** * 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++); } }
/** * 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); }
/** * Keeps track of entity capabilities. * * <p>This work is based on Jonas Adahl's smack fork. * * @author Emil Ivov * @author Lyubomir Marinov */ public class EntityCapsManager { /** * The <tt>Logger</tt> used by the <tt>EntityCapsManager</tt> class and its instances for logging * output. */ private static final Logger logger = Logger.getLogger(EntityCapsManager.class); /** Static OSGi bundle context used by this class. */ private static BundleContext bundleContext; /** Configuration service instance used by this class. */ private static ConfigurationService configService; /** * The prefix of the <tt>ConfigurationService</tt> properties which persist {@link * #caps2discoverInfo}. */ private static final String CAPS_PROPERTY_NAME_PREFIX = "net.java.sip.communicator.impl.protocol.jabber.extensions.caps." + "EntityCapsManager.CAPS."; /** * An empty array of <tt>UserCapsNodeListener</tt> elements explicitly defined in order to reduce * unnecessary allocations. */ private static final UserCapsNodeListener[] NO_USER_CAPS_NODE_LISTENERS = new UserCapsNodeListener[0]; /** The node value to advertise. */ private static String entityNode = OSUtils.IS_ANDROID ? "http://android.jitsi.org" : "http://jitsi.org"; /** * The <tt>Map</tt> of <tt>Caps</tt> to <tt>DiscoverInfo</tt> which associates a node#ver with the * entity capabilities so that they don't have to be retrieved every time their necessary. Because * ver is constructed from the entity capabilities using a specific hash method, the hash method * is also associated with the entity capabilities along with the node and the ver in order to * disambiguate cases of equal ver values for different entity capabilities constructed using * different hash methods. */ private static final Map<Caps, DiscoverInfo> caps2discoverInfo = new ConcurrentHashMap<Caps, DiscoverInfo>(); /** * Map of Full JID -> DiscoverInfo/null. In case of c2s connection the key is formed as * user@server/resource (resource is required) In case of link-local connection the key is formed * as user@host (no resource) */ private final Map<String, Caps> userCaps = new ConcurrentHashMap<String, Caps>(); /** CapsVerListeners gets notified when the version string is changed. */ private final Set<CapsVerListener> capsVerListeners = new CopyOnWriteArraySet<CapsVerListener>(); /** The current hash of our version and supported features. */ private String currentCapsVersion = null; /** * The list of <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the * list of user caps nodes of this <tt>EntityCapsManager</tt>. */ private final List<UserCapsNodeListener> userCapsNodeListeners = new LinkedList<UserCapsNodeListener>(); static { ProviderManager.getInstance() .addExtensionProvider( CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE, new CapsProvider()); } /** * Add {@link DiscoverInfo} to our caps database. * * <p><b>Warning</b>: The specified <tt>DiscoverInfo</tt> is trusted to be valid with respect to * the specified <tt>Caps</tt> for performance reasons because the <tt>DiscoverInfo</tt> should * have already been validated in order to be used elsewhere anyway. * * @param caps the <tt>Caps<tt/> i.e. the node, the hash and the ver for which a * <tt>DiscoverInfo</tt> is to be added to our caps database. * @param info {@link DiscoverInfo} for the specified <tt>Caps</tt>. */ public static void addDiscoverInfoByCaps(Caps caps, DiscoverInfo info) { cleanupDiscoverInfo(info); /* * DiscoverInfo carries the node we're now associating it with a * specific node so we'd better keep them in sync. */ info.setNode(caps.getNodeVer()); synchronized (caps2discoverInfo) { DiscoverInfo oldInfo = caps2discoverInfo.put(caps, info); /* * If the specified info is a new association for the specified * node, remember it across application instances in order to not * query for it over the network. */ if ((oldInfo == null) || !oldInfo.equals(info)) { String xml = info.getChildElementXML(); if ((xml != null) && (xml.length() != 0)) { getConfigService().setProperty(getCapsPropertyName(caps), xml); } } } } /** * Gets the name of the property in the <tt>ConfigurationService</tt> which is or is to be * associated with a specific <tt>Caps</tt> value. * * @param caps the <tt>Caps</tt> value for which the associated <tt>ConfigurationService</tt> * property name is to be returned * @return the name of the property in the <tt>ConfigurationService</tt> which is or is to be * associated with a specific <tt>Caps</tt> value */ private static String getCapsPropertyName(Caps caps) { return CAPS_PROPERTY_NAME_PREFIX + caps.node + '#' + caps.hash + '#' + caps.ver; } /** Returns cached instance of {@link ConfigurationService}. */ private static ConfigurationService getConfigService() { if (configService == null) { configService = ServiceUtils.getService(bundleContext, ConfigurationService.class); } return configService; } /** * Sets OSGi bundle context instance that will be used by this class. * * @param bundleContext the <tt>BundleContext</tt> instance to be used by this class or * <tt>null</tt> to clear the reference. */ public static void setBundleContext(BundleContext bundleContext) { if (bundleContext == null) { configService = null; } EntityCapsManager.bundleContext = bundleContext; } /** * Add a record telling what entity caps node a user has. * * @param user the user (Full JID) * @param node the node (of the caps packet extension) * @param hash the hashing algorithm used to calculate <tt>ver</tt> * @param ver the version (of the caps packet extension) * @param ext the ext (of the caps packet extension) * @param online indicates if the user is online */ private void addUserCapsNode( String user, String node, String hash, String ver, String ext, boolean online) { if ((user != null) && (node != null) && (hash != null) && (ver != null)) { Caps caps = userCaps.get(user); if ((caps == null) || !caps.node.equals(node) || !caps.hash.equals(hash) || !caps.ver.equals(ver)) { caps = new Caps(node, hash, ver, ext); userCaps.put(user, caps); } else return; // Fire userCapsNodeAdded. UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeAdded(user, nodeVer, online); } } } /** * Adds a specific <tt>UserCapsNodeListener</tt> to the list of <tt>UserCapsNodeListener</tt>s * interested in events notifying about changes in the list of user caps nodes of this * <tt>EntityCapsManager</tt>. * * @param listener the <tt>UserCapsNodeListener</tt> which is interested in events notifying about * changes in the list of user caps nodes of this <tt>EntityCapsManager</tt> */ public void addUserCapsNodeListener(UserCapsNodeListener listener) { if (listener == null) throw new NullPointerException("listener"); synchronized (userCapsNodeListeners) { if (!userCapsNodeListeners.contains(listener)) userCapsNodeListeners.add(listener); } } /** * Remove records telling what entity caps node a contact has. * * @param contact the contact */ public void removeContactCapsNode(Contact contact) { Caps caps = null; String lastRemovedJid = null; Iterator<String> iter = userCaps.keySet().iterator(); while (iter.hasNext()) { String jid = iter.next(); if (StringUtils.parseBareAddress(jid).equals(contact.getAddress())) { caps = userCaps.get(jid); lastRemovedJid = jid; iter.remove(); } } // fire only for the last one, at the end the event out // of the protocol will be one and for the contact if (caps != null) { UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeRemoved(lastRemovedJid, nodeVer, false); } } } /** * Remove a record telling what entity caps node a user has. * * @param user the user (Full JID) */ public void removeUserCapsNode(String user) { Caps caps = userCaps.remove(user); // Fire userCapsNodeRemoved. if (caps != null) { UserCapsNodeListener[] listeners; synchronized (userCapsNodeListeners) { listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS); } if (listeners.length != 0) { String nodeVer = caps.getNodeVer(); for (UserCapsNodeListener listener : listeners) listener.userCapsNodeRemoved(user, nodeVer, false); } } } /** * Removes a specific <tt>UserCapsNodeListener</tt> from the list of * <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the list of user * caps nodes of this <tt>EntityCapsManager</tt>. * * @param listener the <tt>UserCapsNodeListener</tt> which is no longer interested in events * notifying about changes in the list of user caps nodes of this <tt>EntityCapsManager</tt> */ public void removeUserCapsNodeListener(UserCapsNodeListener listener) { if (listener != null) { synchronized (userCapsNodeListeners) { userCapsNodeListeners.remove(listener); } } } /** * Gets the <tt>Caps</tt> i.e. the node, the hash and the ver of a user. * * @param user the user (Full JID) * @return the <tt>Caps</tt> i.e. the node, the hash and the ver of <tt>user</tt> */ public Caps getCapsByUser(String user) { return userCaps.get(user); } /** * Get the discover info given a user name. The discover info is returned if the user has a * node#ver associated with it and the node#ver has a discover info associated with it. * * @param user user name (Full JID) * @return the discovered info */ public DiscoverInfo getDiscoverInfoByUser(String user) { Caps caps = userCaps.get(user); return (caps == null) ? null : getDiscoverInfoByCaps(caps); } /** * Get our own caps version. * * @return our own caps version */ public String getCapsVersion() { return currentCapsVersion; } /** * Get our own entity node. * * @return our own entity node. */ public String getNode() { return entityNode; } /** * Set our own entity node. * * @param node the new node */ public void setNode(String node) { entityNode = node; } /** * Retrieve DiscoverInfo for a specific node. * * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver * @return The corresponding DiscoverInfo or null if none is known. */ public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) { synchronized (caps2discoverInfo) { DiscoverInfo discoverInfo = caps2discoverInfo.get(caps); /* * If we don't have the discoverInfo in the runtime cache yet, we * may have it remembered in a previous application instance. */ if (discoverInfo == null) { ConfigurationService configurationService = getConfigService(); String capsPropertyName = getCapsPropertyName(caps); String xml = configurationService.getString(capsPropertyName); if ((xml != null) && (xml.length() != 0)) { IQProvider discoverInfoProvider = (IQProvider) ProviderManager.getInstance() .getIQProvider("query", "http://jabber.org/protocol/disco#info"); if (discoverInfoProvider != null) { XmlPullParser parser = new MXParser(); try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(new StringReader(xml)); // Start the parser. parser.next(); } catch (XmlPullParserException xppex) { parser = null; } catch (IOException ioex) { parser = null; } if (parser != null) { try { discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser); } catch (Exception ex) { } if (discoverInfo != null) { if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo); else { logger.error( "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo); /* * The discoverInfo doesn't seem valid * according to the caps which means that we * must have stored invalid information. * Delete the invalid information in order * to not try to validate it again. */ configurationService.removeProperty(capsPropertyName); } } } } } } return discoverInfo; } } /** * Removes from, to and packet-id from <tt>info</tt>. * * @param info the {@link DiscoverInfo} that we'd like to cleanup. */ private static void cleanupDiscoverInfo(DiscoverInfo info) { info.setFrom(null); info.setTo(null); info.setPacketID(null); } /** * Gets the features of a specific <tt>DiscoverInfo</tt> in the form of a read-only * <tt>Feature</tt> <tt>Iterator<tt/> by calling the internal method {@link * DiscoverInfo#getFeatures()}. * * @param discoverInfo the <tt>DiscoverInfo</tt> the features of which are to be retrieved * @return a read-only <tt>Feature</tt> <tt>Iterator</tt> which lists the features of the * specified <tt>discoverInfo</tt> */ @SuppressWarnings("unchecked") private static Iterator<DiscoverInfo.Feature> getDiscoverInfoFeatures(DiscoverInfo discoverInfo) { Method getFeaturesMethod; try { getFeaturesMethod = DiscoverInfo.class.getDeclaredMethod("getFeatures"); } catch (NoSuchMethodException nsmex) { throw new UndeclaredThrowableException(nsmex); } getFeaturesMethod.setAccessible(true); try { return (Iterator<DiscoverInfo.Feature>) getFeaturesMethod.invoke(discoverInfo); } catch (IllegalAccessException iaex) { throw new UndeclaredThrowableException(iaex); } catch (InvocationTargetException itex) { throw new UndeclaredThrowableException(itex); } } /** * Registers this Manager's listener with <tt>connection</tt>. * * @param connection the connection that we'd like this manager to register with. */ public void addPacketListener(XMPPConnection connection) { PacketFilter filter = new AndFilter( new PacketTypeFilter(Presence.class), new PacketExtensionFilter( CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE)); connection.addPacketListener(new CapsPacketListener(), filter); } /** * Adds <tt>listener</tt> to the list of {@link CapsVerListener}s that we notify when new features * occur and the version hash needs to be regenerated. The method would also notify * <tt>listener</tt> if our current caps version has been generated and is different than * <tt>null</tt>. * * @param listener the {@link CapsVerListener} we'd like to register. */ public void addCapsVerListener(CapsVerListener listener) { synchronized (capsVerListeners) { if (capsVerListeners.contains(listener)) return; capsVerListeners.add(listener); if (currentCapsVersion != null) listener.capsVerUpdated(currentCapsVersion); } } /** * Removes <tt>listener</tt> from the list of currently registered {@link CapsVerListener}s. * * @param listener the {@link CapsVerListener} we'd like to unregister. */ public void removeCapsVerListener(CapsVerListener listener) { synchronized (capsVerListeners) { capsVerListeners.remove(listener); } } /** * Notifies all currently registered {@link CapsVerListener}s that the version hash has changed. */ private void fireCapsVerChanged() { List<CapsVerListener> listenersCopy = null; synchronized (capsVerListeners) { listenersCopy = new ArrayList<CapsVerListener>(capsVerListeners); } for (CapsVerListener listener : listenersCopy) listener.capsVerUpdated(currentCapsVersion); } /** * Computes and returns the hash of the specified <tt>capsString</tt> using the specified * <tt>hashAlgorithm</tt>. * * @param hashAlgorithm the name of the algorithm to be used to generate the hash * @param capsString the capabilities string that we'd like to compute a hash for. * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or * <tt>null</tt> if generating the hash has failed */ private static String capsToHash(String hashAlgorithm, String capsString) { try { MessageDigest md = MessageDigest.getInstance(hashAlgorithm); byte[] digest = md.digest(capsString.getBytes()); return Base64.encodeBytes(digest); } catch (NoSuchAlgorithmException nsae) { logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm); return null; } } /** * Converts the form field values in the <tt>ffValuesIter</tt> into a caps string. * * @param ffValuesIter the {@link Iterator} containing the form field values. * @param capsBldr a <tt>StringBuilder</tt> to which the caps string representing the form field * values is to be appended */ private static void formFieldValuesToCaps(Iterator<String> ffValuesIter, StringBuilder capsBldr) { SortedSet<String> fvs = new TreeSet<String>(); while (ffValuesIter.hasNext()) fvs.add(ffValuesIter.next()); for (String fv : fvs) capsBldr.append(fv).append('<'); } /** * Calculates the <tt>String</tt> for a specific <tt>DiscoverInfo</tt> which is to be hashed in * order to compute the ver string for that <tt>DiscoverInfo</tt>. * * @param discoverInfo the <tt>DiscoverInfo</tt> for which the <tt>String</tt> to be hashed in * order to compute its ver string is to be calculated * @return the <tt>String</tt> for <tt>discoverInfo</tt> which is to be hashed in order to compute * its ver string */ private static String calculateEntityCapsString(DiscoverInfo discoverInfo) { StringBuilder bldr = new StringBuilder(); // Add identities { Iterator<DiscoverInfo.Identity> identities = discoverInfo.getIdentities(); SortedSet<DiscoverInfo.Identity> is = new TreeSet<DiscoverInfo.Identity>( new Comparator<DiscoverInfo.Identity>() { public int compare(DiscoverInfo.Identity i1, DiscoverInfo.Identity i2) { int category = i1.getCategory().compareTo(i2.getCategory()); if (category != 0) return category; int type = i1.getType().compareTo(i2.getType()); if (type != 0) return type; /* * TODO Sort by xml:lang. * * Since sort by xml:lang is currently missing, * use the last supported sort criterion i.e. * type. */ return type; } }); if (identities != null) while (identities.hasNext()) is.add(identities.next()); for (DiscoverInfo.Identity i : is) { bldr.append(i.getCategory()) .append('/') .append(i.getType()) .append("//") .append(i.getName()) .append('<'); } } // Add features { Iterator<DiscoverInfo.Feature> features = getDiscoverInfoFeatures(discoverInfo); SortedSet<String> fs = new TreeSet<String>(); if (features != null) while (features.hasNext()) fs.add(features.next().getVar()); for (String f : fs) bldr.append(f).append('<'); } DataForm extendedInfo = (DataForm) discoverInfo.getExtension("x", "jabber:x:data"); if (extendedInfo != null) { synchronized (extendedInfo) { SortedSet<FormField> fs = new TreeSet<FormField>( new Comparator<FormField>() { public int compare(FormField f1, FormField f2) { return f1.getVariable().compareTo(f2.getVariable()); } }); FormField formType = null; for (Iterator<FormField> fieldsIter = extendedInfo.getFields(); fieldsIter.hasNext(); ) { FormField f = fieldsIter.next(); if (!f.getVariable().equals("FORM_TYPE")) fs.add(f); else formType = f; } // Add FORM_TYPE values if (formType != null) formFieldValuesToCaps(formType.getValues(), bldr); // Add the other values for (FormField f : fs) { bldr.append(f.getVariable()).append('<'); formFieldValuesToCaps(f.getValues(), bldr); } } } return bldr.toString(); } /** * Calculates the ver string for the specified <tt>discoverInfo</tt>, identity type, name * features, and extendedInfo. * * @param discoverInfo the {@link DiscoverInfo} we'd be creating a ver <tt>String</tt> for */ public void calculateEntityCapsVersion(DiscoverInfo discoverInfo) { setCurrentCapsVersion( discoverInfo, capsToHash(CapsPacketExtension.HASH_METHOD, calculateEntityCapsString(discoverInfo))); } /** * Set our own caps version. * * @param discoverInfo the {@link DiscoverInfo} that we'd like to map to the <tt>capsVersion</tt>. * @param capsVersion the new caps version */ public void setCurrentCapsVersion(DiscoverInfo discoverInfo, String capsVersion) { Caps caps = new Caps(getNode(), CapsPacketExtension.HASH_METHOD, capsVersion, null); /* * DiscoverInfo carries the node and the ver and we're now setting a new * ver so we should update the DiscoveryInfo. */ discoverInfo.setNode(caps.getNodeVer()); if (!caps.isValid(discoverInfo)) { throw new IllegalArgumentException( "The specified discoverInfo must be valid with respect" + " to the specified capsVersion"); } currentCapsVersion = capsVersion; addDiscoverInfoByCaps(caps, discoverInfo); fireCapsVerChanged(); } /** The {@link PacketListener} that will be registering incoming caps. */ private class CapsPacketListener implements PacketListener { /** * Handles incoming presence packets and maps jids to node#ver strings. * * @param packet the incoming presence <tt>Packet</tt> to be handled * @see PacketListener#processPacket(Packet) */ public void processPacket(Packet packet) { CapsPacketExtension ext = (CapsPacketExtension) packet.getExtension(CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE); /* * Before Version 1.4 of XEP-0115: Entity Capabilities, the 'ver' * attribute was generated differently and the 'hash' attribute was * absent. The 'ver' attribute in Version 1.3 represents the * specific version of the client and thus does not provide a way to * validate the DiscoverInfo sent by the client. If * EntityCapsManager receives no 'hash' attribute, it will assume * the legacy format and will not cache it because the DiscoverInfo * to be received from the client later on will not be trustworthy. */ String hash = ext.getHash(); /* Google Talk web does not set hash but we need it to be cached */ if (hash == null) hash = ""; if (hash != null) { // Check it the packet indicates that the user is online. We // will use this information to decide if we're going to send // the discover info request. boolean online = (packet instanceof Presence) && ((Presence) packet).isAvailable(); if (online) { addUserCapsNode( packet.getFrom(), ext.getNode(), hash, ext.getVersion(), ext.getExtensions(), online); } else { removeUserCapsNode(packet.getFrom()); } } } } /** * Implements an immutable value which stands for a specific node, a specific hash (algorithm) and * a specific ver. * * @author Lyubomir Marinov */ public static class Caps { /** The hash (algorithm) of this <tt>Caps</tt> value. */ public final String hash; /** The node of this <tt>Caps</tt> value. */ public final String node; /** The ext info of this <tt>Caps</tt> value. */ public String ext; /** * The String which is the concatenation of {@link #node} and the {@link #ver} separated by the * character '#'. Cached for the sake of efficiency. */ private final String nodeVer; /** The ver of this <tt>Caps</tt> value. */ public final String ver; /** * Initializes a new <tt>Caps</tt> instance which is to represent a specific node, a specific * hash (algorithm) and a specific ver. * * @param node the node to be represented by the new instance * @param hash the hash (algorithm) to be represented by the new instance * @param ver the ver to be represented by the new instance * @param ext the ext to be represented by the new instance */ public Caps(String node, String hash, String ver, String ext) { if (node == null) throw new NullPointerException("node"); if (hash == null) throw new NullPointerException("hash"); if (ver == null) throw new NullPointerException("ver"); this.node = node; this.hash = hash; this.ver = ver; this.ext = ext; this.nodeVer = this.node + '#' + this.ver; } /** * Gets a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of * this instance, the character '#' and the <tt>ver</tt> property of this instance. * * @return a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of * this instance, the character '#' and the <tt>ver</tt> property of this instance */ public final String getNodeVer() { return nodeVer; } /** * Determines whether a specific <tt>DiscoverInfo</tt> is valid according to this <tt>Caps</tt> * i.e. whether the <tt>discoverInfo</tt> has the node and the ver of this <tt>Caps</tt> and the * ver calculated from the <tt>discoverInfo</tt> using the hash (algorithm) of this * <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>. * * @param discoverInfo the <tt>DiscoverInfo</tt> to be validated by this <tt>Caps</tt> * @return <tt>true</tt> if the specified <tt>DiscoverInfo</tt> has the node and the ver of this * <tt>Caps</tt> and the ver calculated from the <tt>discoverInfo</tt> using the hash * (algorithm) of this <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>; otherwise, * <tt>false</tt> */ public boolean isValid(DiscoverInfo discoverInfo) { if (discoverInfo != null) { // The "node" attribute is not necessary in the query element. // For example, Swift does not send back the "node" attribute in // the Disco#info response. Thus, if the node of the IQ response // is null, then we set it to the request one. if (discoverInfo.getNode() == null) { discoverInfo.setNode(getNodeVer()); } if (getNodeVer().equals(discoverInfo.getNode()) && !hash.equals("") && ver.equals(capsToHash(hash, calculateEntityCapsString(discoverInfo)))) { return true; } } return false; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Caps caps = (Caps) o; if (!hash.equals(caps.hash)) return false; if (!node.equals(caps.node)) return false; if (!ver.equals(caps.ver)) return false; return true; } @Override public int hashCode() { int result = hash.hashCode(); result = 31 * result + node.hashCode(); result = 31 * result + ver.hashCode(); return result; } } }
/** * Retrieve DiscoverInfo for a specific node. * * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver * @return The corresponding DiscoverInfo or null if none is known. */ public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) { synchronized (caps2discoverInfo) { DiscoverInfo discoverInfo = caps2discoverInfo.get(caps); /* * If we don't have the discoverInfo in the runtime cache yet, we * may have it remembered in a previous application instance. */ if (discoverInfo == null) { ConfigurationService configurationService = getConfigService(); String capsPropertyName = getCapsPropertyName(caps); String xml = configurationService.getString(capsPropertyName); if ((xml != null) && (xml.length() != 0)) { IQProvider discoverInfoProvider = (IQProvider) ProviderManager.getInstance() .getIQProvider("query", "http://jabber.org/protocol/disco#info"); if (discoverInfoProvider != null) { XmlPullParser parser = new MXParser(); try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(new StringReader(xml)); // Start the parser. parser.next(); } catch (XmlPullParserException xppex) { parser = null; } catch (IOException ioex) { parser = null; } if (parser != null) { try { discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser); } catch (Exception ex) { } if (discoverInfo != null) { if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo); else { logger.error( "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo); /* * The discoverInfo doesn't seem valid * according to the caps which means that we * must have stored invalid information. * Delete the invalid information in order * to not try to validate it again. */ configurationService.removeProperty(capsPropertyName); } } } } } } return discoverInfo; } }