From d47463a5332fc06ee87f5aac4bf378f9629368f1 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Wed, 11 Jan 2017 19:35:55 +0100 Subject: [PATCH] Deprecate Chat API, introduce new Chat API Also add (From|To)TypeFilter and update/fix the documentation in a few places. --- documentation/extensions/index.md | 1 + documentation/extensions/xhtml.md | 23 +- documentation/gettingstarted.md | 20 +- documentation/messaging.md | 76 ++---- .../smack/filter/AbstractJidTypeFilter.java | 62 +++++ .../smack/filter/FromTypeFilter.java | 38 +++ .../smack/filter/ToTypeFilter.java | 40 +++ .../smackx/muclight/MultiUserChatLight.java | 8 +- .../org/jivesoftware/smack/chat2/Chat.java | 73 ++++++ .../jivesoftware/smack/chat2/ChatManager.java | 237 ++++++++++++++++++ .../chat2/IncomingChatMessageListener.java | 26 ++ .../chat2/OutgoingChatMessageListener.java | 26 ++ .../smack/chat2/package-info.java | 21 ++ .../smackx/chatstates/ChatStateListener.java | 5 +- .../smackx/chatstates/ChatStateManager.java | 22 +- .../smackx/muc/MultiUserChat.java | 9 +- .../org/jivesoftware/smack/chat/Chat.java | 2 + .../jivesoftware/smack/chat/ChatManager.java | 2 + .../smack/chat/ChatManagerListener.java | 1 + .../smack/chat/ChatMessageListener.java | 1 + .../smack/chat/ChatConnectionTest.java | 18 +- .../java/org/jivesoftware/smack/ChatTest.java | 14 +- 22 files changed, 612 insertions(+), 113 deletions(-) create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/AbstractJidTypeFilter.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/FromTypeFilter.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/filter/ToTypeFilter.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smack/chat2/Chat.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smack/chat2/ChatManager.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smack/chat2/IncomingChatMessageListener.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smack/chat2/OutgoingChatMessageListener.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smack/chat2/package-info.java diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 1cf51a067..481229840 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -69,6 +69,7 @@ Smack Extensions and currently supported XEPs of smack-extensions | XMPP Over BOSH | [XEP-0206](http://xmpp.org/extensions/xep-0206.html) | Use Bidirectional-streams Over Synchronous HTTP (BOSH) to transport XMPP stanzas. | | Attention | [XEP-0224](http://xmpp.org/extensions/xep-0224.html) | Getting attention of another user. | | Bits of Binary | [XEP-0231](http://xmpp.org/extensions/xep-0231.html) | Including or referring to small bits of binary data in an XML stanza. | +| Best Practices for Resource Locking | [XEP-0296](https://xmpp.org/extensions/xep-0296.html) | Specifies best practices to be followed by Jabber/XMPP clients about when to lock into, and unlock away from, resources. | | Last Message Correction | [XEP-0308](http://xmpp.org/extensions/xep-0308.html) | Provides a method for indicating that a message is a correction of the last sent message. | | [Group Chat Invitations](invitation.md) | n/a | Send invitations to other users to join a group chat room. | | [Jive Properties](properties.md) | n/a | TODO | diff --git a/documentation/extensions/xhtml.md b/documentation/extensions/xhtml.md index 62580bd97..a947e6ada 100644 --- a/documentation/extensions/xhtml.md +++ b/documentation/extensions/xhtml.md @@ -97,8 +97,8 @@ done. The last step is to send the message as you do with any other message. An XHTML message is like any regular message, therefore to send the message you can follow the usual steps you do in order to send a message. For example, -to send a message as part of a chat just use the message **#send(Message)** of -_**Chat**_ or you can use the message **#send(Stanza)** of +to send a message as part of a chat just use the message **#sendMessage(Message)** of +_**Chat**_ or you can use the message **#sendStanza(Stanza)** of _**XMPPConnection**_. **Example** @@ -142,19 +142,20 @@ XHTML bodies of any received message. ``` // Create a listener for the chat and display any XHTML content -PacketListener packetListener = new PacketListener() { -public void processStanza(Stanza stanza) { - Message message = (Message) stanza; +IncomingChatMessageListener listener = new IncomingChatMessageListener() { +public void newIncomingMessage(EntityBareJid from, Message message, Chat chat) { // Obtain the XHTML bodies of the message List bodies = XHTMLManager.getBodies(message); - if (bodies != null) { - // Display the bodies on the console - for (CharSequence body : bodies) { - System.out.println(body); - } + if (bodies == null) { + return; + + // Display the bodies on the console + for (CharSequence body : bodies) { + System.out.println(body); } +} }; -chat.addMessageListener(packetListener); +chatManager.addListener(listener); ``` Discover support for XHTML Messages diff --git a/documentation/gettingstarted.md b/documentation/gettingstarted.md index 98879bdaf..c006950e7 100644 --- a/documentation/gettingstarted.md +++ b/documentation/gettingstarted.md @@ -70,7 +70,7 @@ created, such as the ability to disable or require encryption. See Once you've created a connection, you should login with the `XMPPConnection.login()` method. Once you've logged in, you can being -chatting with other users by creating new `Chat` or `GroupChat` +chatting with other users by creating new `Chat` or `MultiUserChat` objects. Working with the Roster @@ -98,18 +98,18 @@ your presence to let people know you're unavailable and "out fishing": // Create a new presence. Pass in false to indicate we're unavailable._ Presence presence = new Presence(Presence.Type.unavailable); presence.setStatus("Gone fishing"); -// Send the packet (assume we have an XMPPConnection instance called "con"). +// Send the stanza (assume we have an XMPPConnection instance called "con"). con.sendStanza(presence); ``` -Smack provides two ways to read incoming packets: `PacketListener`, and -`PacketCollector`. Both use `StanzaFilter` instances to determine which -packets should be processed. A packet listener is used for event style -programming, while a packet collector has a result queue of packets that you -can do polling and blocking operations on. So, a packet listener is useful -when you want to take some action whenever a packet happens to come in, while -a packet collector is useful when you want to wait for a specific packet to -arrive. Packet collectors and listeners can be created using an Connection +Smack provides two ways to read incoming packets: `StanzaListener`, and +`StanzaCollector`. Both use `StanzaFilter` instances to determine which +stanzas should be processed. A stanza listener is used for event style +programming, while a stanza collector has a result queue of packets that you +can do polling and blocking operations on. So, a stanza listener is useful +when you want to take some action whenever a stanza happens to come in, while +a stanza collector is useful when you want to wait for a specific packet to +arrive. Stanza collectors and listeners can be created using an Connection instance. Copyright (C) Jive Software 2002-2008 diff --git a/documentation/messaging.md b/documentation/messaging.md index 666bbecef..72302ef61 100644 --- a/documentation/messaging.md +++ b/documentation/messaging.md @@ -6,7 +6,7 @@ Messaging using Chats Sending messages back and forth is at the core of instant messaging. Although individual messages can be sent and received as packets, it's generally easier to treat the string of messages as a chat using the -`org.jivesoftware.smack.Chat` class. +`org.jivesoftware.smack.chat2.Chat` class. Chat ---- @@ -17,80 +17,44 @@ and then send them a text message: ``` // Assume we've created an XMPPConnection name "connection"._ -ChatManager chatmanager = ChatManager.getInstanceFor(connection); -Chat newChat = chatmanager.createChat("jsmith@jivesoftware.com", new MessageListener() { - public void processMessage(Chat chat, Message message) { - System.out.println("Received message: " + message); - } +ChatManager chatManager = ChatManager.getInstanceFor(connection); +chatManager.addListener(new IncomingChatMessageListener() { + @Override + void newIncomingMessage(EntityBareJid from, Message message, Chat chat) { + System.out.println("New message from " + from ": " + message.getBody()); + } }); - -try { - newChat.sendMessage("Howdy!"); -} -catch (XMPPException e) { - System.out.println("Error Delivering block"); +EntityBareJid jid = JidCreate.entityBareFrom("jsmith@jivesoftware.com"); +Chat chat = chatManager.chatWith(jid); +chat.sendMessage("Howdy!"); } ``` The `Chat.sendMessage(String)` method is a convenience method that creates a Message object, sets the body using the String parameter, then sends the message. In the case that you wish to set additional values on a Message -before sending it, use the `Chat.createMessage()` and -`Chat.sendMessage(Message)` methods, as in the following code snippet: +before sending it, use +`Chat.sendMessage(Message)` method, as in the following code snippet: ``` Message newMessage = new Message(); newMessage.setBody("Howdy!"); // Additional modifications to the message Stanza. JivePropertiesManager.addProperty(newMessage, "favoriteColor", "red"); -newChat.sendMessage(newMessage); +chat.sendMessage(newMessage); ``` -You'll also notice in the example above that we specified a MessageListener -when creating a chat. The listener is notified any time a new message arrives -from the other user in the chat. The following code snippet uses the listener +You'll also notice in the example above that we specified an IncomingChatMessageListener. +The listener is notified any time a new chat message arrives. +The following code snippet uses the listener as a parrot-bot -- it echoes back everything the other user types. ``` -// Assume a MessageListener we've setup with a chat._ - -public void processMessage(Chat chat, Message message) { - // Send back the same text the other user sent us._ - chat.sendMessage(message.getBody()); +// Assume a IncomingChatMessageListener we've setup with a ChatManager +public void newIncomingMessage(EntityBareJid from, Message message, Chat chat) { + // Send back the same text the other user sent us. + chat.sendMessage(message.getBody()); } ``` -Incoming Chat -------------- - -When chats are prompted by another user, the setup is slightly different since -you are receiving a chat message first. Instead of explicitly creating a chat -to send messages, you need to register to handle newly created Chat instances -when the ChatManager creates them. The ChatManager will already find a -matching chat (by thread id) and if none exists, then it will create a new one -that does match. To get this new chat, you have to register to be notified -when it happens. You can register a message listener to receive all future -messages as part of this handler. - -``` -// Assume we've created an XMPPConnection name "connection"._ -ChatManager chatManager = ChatManager.getInstanceFor(connection); -chatManager.addChatListener( - new ChatManagerListener() { - @Override - public void chatCreated(Chat chat, boolean createdLocally) - { - if (!createdLocally) - chat.addMessageListener(new MyNewMessageListener());; - } - }); -``` - -In addition to thread based chat messages, there are some clients that do not -send a thread id as part of the chat. To handle this scenario, Smack will -attempt match the incoming messages to the best fit existing chat, based on -the JID. It will attempt to find a chat with the same full JID, failing that, -it will try the base JID. If no existing chat to the user can found, then a -new one is created. - Copyright (C) Jive Software 2002-2008 diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/AbstractJidTypeFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/AbstractJidTypeFilter.java new file mode 100644 index 000000000..e091d6786 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/AbstractJidTypeFilter.java @@ -0,0 +1,62 @@ +/** + * + * Copyright 2017 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Stanza; +import org.jxmpp.jid.Jid; + +public abstract class AbstractJidTypeFilter implements StanzaFilter { + + protected enum JidType { + entityFull, + entityBare, + domainFull, + domainBare, + ; + } + + private final JidType jidType; + + protected AbstractJidTypeFilter(JidType jidType) { + this.jidType = jidType; + } + + protected abstract Jid getJidToInspect(Stanza stanza); + + @Override + public final boolean accept(Stanza stanza) { + final Jid jid = stanza.getFrom(); + + if (jid == null) { + return false; + } + + switch (jidType) { + case entityFull: + return jid.isEntityFullJid(); + case entityBare: + return jid.isEntityBareJid(); + case domainFull: + return jid.isDomainFullJid(); + case domainBare: + return jid.isDomainBareJid(); + default: + throw new AssertionError(); + } + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/FromTypeFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/FromTypeFilter.java new file mode 100644 index 000000000..1948d5f84 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/FromTypeFilter.java @@ -0,0 +1,38 @@ +/** + * + * Copyright 2017 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Stanza; +import org.jxmpp.jid.Jid; + +public final class FromTypeFilter extends AbstractJidTypeFilter { + + public static final FromTypeFilter ENTITY_FULL_JID = new FromTypeFilter(JidType.entityFull); + public static final FromTypeFilter ENTITY_BARE_JID = new FromTypeFilter(JidType.entityBare); + public static final FromTypeFilter DOMAIN_FULL_JID = new FromTypeFilter(JidType.domainFull); + public static final FromTypeFilter DOMAIN_BARE_JID = new FromTypeFilter(JidType.domainBare); + + private FromTypeFilter(JidType jidType) { + super(jidType); + } + + @Override + protected Jid getJidToInspect(Stanza stanza) { + return stanza.getFrom(); + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/filter/ToTypeFilter.java b/smack-core/src/main/java/org/jivesoftware/smack/filter/ToTypeFilter.java new file mode 100644 index 000000000..8d8059627 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/filter/ToTypeFilter.java @@ -0,0 +1,40 @@ +/** + * + * Copyright 2017 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Stanza; +import org.jxmpp.jid.Jid; + +public final class ToTypeFilter extends AbstractJidTypeFilter { + + public static final ToTypeFilter ENTITY_FULL_JID = new ToTypeFilter(JidType.entityFull); + public static final ToTypeFilter ENTITY_BARE_JID = new ToTypeFilter(JidType.entityBare); + public static final ToTypeFilter DOMAIN_FULL_JID = new ToTypeFilter(JidType.domainFull); + public static final ToTypeFilter DOMAIN_BARE_JID = new ToTypeFilter(JidType.domainBare); + + public static final StanzaFilter ENTITY_FULL_OR_BARE_JID = new OrFilter(ENTITY_FULL_JID, ENTITY_BARE_JID); + + private ToTypeFilter(JidType jidType) { + super(jidType); + } + + @Override + protected Jid getJidToInspect(Stanza stanza) { + return stanza.getTo(); + } + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java index a2ee01441..e14f9df11 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/muclight/MultiUserChatLight.java @@ -28,8 +28,6 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.chat.Chat; -import org.jivesoftware.smack.chat.ChatManager; import org.jivesoftware.smack.chat.ChatMessageListener; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.FromMatchesFilter; @@ -146,8 +144,10 @@ public class MultiUserChatLight { * for the newly created chat. * @return new Chat for sending private messages to a given room occupant. */ - public Chat createPrivateChat(EntityJid occupant, ChatMessageListener listener) { - return ChatManager.getInstanceFor(connection).createChat(occupant, listener); + @Deprecated + // Do not re-use Chat API, which was designed for XMPP-IM 1:1 chats and not MUClight private chats. + public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityJid occupant, ChatMessageListener listener) { + return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener); } /** diff --git a/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/Chat.java b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/Chat.java new file mode 100644 index 000000000..f8b9edbd6 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/Chat.java @@ -0,0 +1,73 @@ +/** + * + * Copyright 2017 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.chat2; + +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Presence; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; +import org.jxmpp.jid.Jid; + +public final class Chat extends Manager { + + private final EntityBareJid jid; + + volatile EntityFullJid lockedResource; + + Presence lastPresenceOfLockedResource; + + Chat(final XMPPConnection connection, EntityBareJid jid) { + super(connection); + this.jid = jid; + } + + public void send(CharSequence message) throws NotConnectedException, InterruptedException { + Message stanza = new Message(); + stanza.setBody(message); + send(stanza); + } + + public void send(Message message) throws NotConnectedException, InterruptedException { + switch (message.getType()) { + case normal: + case chat: + break; + default: + throw new IllegalArgumentException("Message must be of type 'normal' or 'chat'"); + } + + Jid to = lockedResource; + if (to == null) { + to = jid; + } + message.setTo(to); + + connection().sendStanza(message); + } + + public EntityBareJid getXmppAddressOfChatPartner() { + return jid; + } + + void unlockResource() { + lockedResource = null; + lastPresenceOfLockedResource = null; + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/ChatManager.java b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/ChatManager.java new file mode 100644 index 000000000..b19dd290f --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/ChatManager.java @@ -0,0 +1,237 @@ +/** + * + * Copyright 2017 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.chat2; + +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromTypeFilter; +import org.jivesoftware.smack.filter.MessageTypeFilter; +import org.jivesoftware.smack.filter.MessageWithBodiesFilter; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.StanzaExtensionFilter; +import org.jivesoftware.smack.filter.StanzaFilter; +import org.jivesoftware.smack.filter.ToTypeFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.roster.AbstractRosterListener; +import org.jivesoftware.smack.roster.Roster; +import org.jivesoftware.smackx.xhtmlim.packet.XHTMLExtension; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.EntityFullJid; +import org.jxmpp.jid.Jid; + +/** + * A chat manager for 1:1 XMPP instant messaging chats. + *

+ * This manager and the according {@link Chat} API implement "Resource Locking" (XEP-0296). Support for Carbon Copies + * (XEP-0280) will be added once the XEP has progressed from experimental. + *

+ * + * @see XEP-0296: Best Practices for Resource Locking + */ +public final class ChatManager extends Manager { + + private static final Map INSTANCES = new WeakHashMap<>(); + + public static synchronized ChatManager getInstanceFor(XMPPConnection connection) { + ChatManager chatManager = INSTANCES.get(connection); + if (chatManager == null) { + chatManager = new ChatManager(connection); + INSTANCES.put(connection, chatManager); + } + return chatManager; + } + + // @FORMATTER:OFF + private static final StanzaFilter MESSAGE_FILTER = new AndFilter( + MessageTypeFilter.NORMAL_OR_CHAT, + new OrFilter(MessageWithBodiesFilter.INSTANCE), new StanzaExtensionFilter(XHTMLExtension.ELEMENT, XHTMLExtension.NAMESPACE) + ); + + private static final StanzaFilter OUTGOING_MESSAGE_FILTER = new AndFilter( + MESSAGE_FILTER, + ToTypeFilter.ENTITY_FULL_OR_BARE_JID + ); + + private static final StanzaFilter INCOMING_MESSAGE_FILTER = new AndFilter( + MESSAGE_FILTER, + FromTypeFilter.ENTITY_FULL_JID + ); + // @FORMATTER:ON + + private final Map chats = new ConcurrentHashMap<>(); + + private final Set incomingListeners = new CopyOnWriteArraySet<>(); + + private final Set outgoingListeners = new CopyOnWriteArraySet<>(); + + private boolean xhtmlIm; + + private ChatManager(final XMPPConnection connection) { + super(connection); + connection.addSyncStanzaListener(new StanzaListener() { + @Override + public void processStanza(Stanza stanza) { + Message message = (Message) stanza; + if (!shouldAcceptMessage(message)) { + return; + } + + final Jid from = message.getFrom(); + final EntityFullJid fullFrom = from.asEntityFullJidOrThrow(); + final EntityBareJid bareFrom = fullFrom.asEntityBareJid(); + final Chat chat = chatWith(bareFrom); + chat.lockedResource = fullFrom; + + for (IncomingChatMessageListener listener : incomingListeners) { + listener.newIncomingMessage(bareFrom, message, chat); + } + } + }, INCOMING_MESSAGE_FILTER); + + connection.addPacketInterceptor(new StanzaListener() { + @Override + public void processStanza(Stanza stanza) throws NotConnectedException, InterruptedException { + Message message = (Message) stanza; + if (!shouldAcceptMessage(message)) { + return; + } + + final EntityBareJid to = message.getTo().asEntityBareJidOrThrow(); + final Chat chat = chatWith(to); + + for (OutgoingChatMessageListener listener : outgoingListeners) { + listener.newOutgoingMessage(to, message, chat); + } + } + }, OUTGOING_MESSAGE_FILTER); + + Roster roster = Roster.getInstanceFor(connection); + roster.addRosterListener(new AbstractRosterListener() { + @Override + public void presenceChanged(Presence presence) { + final Jid from = presence.getFrom(); + final EntityBareJid bareFrom = from.asEntityBareJidIfPossible(); + if (bareFrom == null) { + return; + } + + final Chat chat = chats.get(bareFrom); + if (chat == null) { + return; + } + + if (chat.lockedResource == null) { + // According to XEP-0296, no action is required for resource locking upon receiving a presence if no + // resource is currently locked. + return; + } + + final EntityFullJid fullFrom = from.asEntityFullJidIfPossible(); + if (!chat.lockedResource.equals(fullFrom)) { + return; + } + + if (chat.lastPresenceOfLockedResource == null) { + // We have no last known presence from the locked resource. + chat.lastPresenceOfLockedResource = presence; + return; + } + + if (chat.lastPresenceOfLockedResource.getMode() != presence.getMode() + || chat.lastPresenceOfLockedResource.getType() != presence.getType()) { + chat.unlockResource(); + } + } + }); + } + + private boolean shouldAcceptMessage(Message message) { + if (!message.getBodies().isEmpty()) { + return true; + } + + // Message has no XMPP-IM bodies, abort here if xhtmlIm is not enabled. + if (!xhtmlIm) { + return false; + } + + XHTMLExtension xhtmlExtension = XHTMLExtension.from(message); + if (xhtmlExtension == null) { + // Message has no XHTML-IM extension, abort. + return false; + } + return true; + } + + public boolean addListener(IncomingChatMessageListener listener) { + return incomingListeners.add(listener); + } + + public boolean removeListener(IncomingChatMessageListener listener) { + return incomingListeners.remove(listener); + } + + public boolean addListener(OutgoingChatMessageListener listener) { + return outgoingListeners.add(listener); + } + + public boolean removeOutoingLIstener(OutgoingChatMessageListener listener) { + return outgoingListeners.remove(listener); + } + + /** + * Start a new or retrieve the existing chat with jid. + * + * @param jid the XMPP address of the other entity to chat with. + * @return the Chat API for the given XMPP address. + */ + public Chat chatWith(EntityBareJid jid) { + Chat chat = chats.get(jid); + if (chat == null) { + synchronized (chats) { + // Double-checked locking. + chat = chats.get(jid); + if (chat != null) { + return chat; + } + chat = new Chat(connection(), jid); + chats.put(jid, chat); + } + } + return chat; + } + + /** + * Also notify about messages containing XHTML-IM. + * + * @param xhtmlIm + */ + public void setXhmtlImEnabled(boolean xhtmlIm) { + this.xhtmlIm = xhtmlIm; + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/IncomingChatMessageListener.java b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/IncomingChatMessageListener.java new file mode 100644 index 000000000..a59e7b057 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/IncomingChatMessageListener.java @@ -0,0 +1,26 @@ +/** + * + * Copyright 2017 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.chat2; + +import org.jivesoftware.smack.packet.Message; +import org.jxmpp.jid.EntityBareJid; + +public interface IncomingChatMessageListener { + + void newIncomingMessage(EntityBareJid from, Message message, Chat chat); + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/OutgoingChatMessageListener.java b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/OutgoingChatMessageListener.java new file mode 100644 index 000000000..74660cb56 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/OutgoingChatMessageListener.java @@ -0,0 +1,26 @@ +/** + * + * Copyright 2017 Florian Schmaus. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.chat2; + +import org.jivesoftware.smack.packet.Message; +import org.jxmpp.jid.EntityBareJid; + +public interface OutgoingChatMessageListener { + + void newOutgoingMessage(EntityBareJid to, Message message, Chat chat); + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/package-info.java new file mode 100644 index 000000000..e189afa10 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smack/chat2/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright 2017 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Smack's new improved API for 1:1 chats. + */ +package org.jivesoftware.smack.chat2; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateListener.java index d0c671b95..c670f55ac 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateListener.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateListener.java @@ -17,7 +17,6 @@ package org.jivesoftware.smackx.chatstates; -import org.jivesoftware.smack.chat.Chat; import org.jivesoftware.smack.chat.ChatMessageListener; import org.jivesoftware.smack.packet.Message; @@ -35,5 +34,7 @@ public interface ChatStateListener extends ChatMessageListener { * @param state the new state of the participant. * @param message the message carrying the chat state. */ - void stateChanged(Chat chat, ChatState state, Message message); + // TODO Migrate to new chat2 API on Smack 4.3. + @SuppressWarnings("deprecation") + void stateChanged(org.jivesoftware.smack.chat.Chat chat, ChatState state, Message message); } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateManager.java index ec15c1910..212ad0918 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/chatstates/ChatStateManager.java @@ -24,8 +24,6 @@ import org.jivesoftware.smack.MessageListener; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.Manager; -import org.jivesoftware.smack.chat.Chat; -import org.jivesoftware.smack.chat.ChatManager; import org.jivesoftware.smack.chat.ChatManagerListener; import org.jivesoftware.smack.chat.ChatMessageListener; import org.jivesoftware.smack.filter.NotFilter; @@ -49,6 +47,8 @@ import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; * @see org.jivesoftware.smackx.chatstates.ChatState * @see org.jivesoftware.smackx.chatstates.packet.ChatStateExtension */ +// TODO Migrate to new chat2 API on Smack 4.3. +@SuppressWarnings("deprecation") public final class ChatStateManager extends Manager { public static final String NAMESPACE = "http://jabber.org/protocol/chatstates"; @@ -79,13 +79,13 @@ public final class ChatStateManager extends Manager { /** * Maps chat to last chat state. */ - private final Map chatStates = new WeakHashMap(); + private final Map chatStates = new WeakHashMap<>(); - private final ChatManager chatManager; + private final org.jivesoftware.smack.chat.ChatManager chatManager; private ChatStateManager(XMPPConnection connection) { super(connection); - chatManager = ChatManager.getInstanceFor(connection); + chatManager = org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection); chatManager.addOutgoingMessageInterceptor(outgoingInterceptor, filter); chatManager.addChatListener(incomingInterceptor); @@ -104,7 +104,7 @@ public final class ChatStateManager extends Manager { * @throws NotConnectedException * @throws InterruptedException */ - public void setCurrentState(ChatState newState, Chat chat) throws NotConnectedException, InterruptedException { + public void setCurrentState(ChatState newState, org.jivesoftware.smack.chat.Chat chat) throws NotConnectedException, InterruptedException { if(chat == null || newState == null) { throw new IllegalArgumentException("Arguments cannot be null."); } @@ -133,7 +133,7 @@ public final class ChatStateManager extends Manager { return connection().hashCode(); } - private synchronized boolean updateChatState(Chat chat, ChatState newState) { + private synchronized boolean updateChatState(org.jivesoftware.smack.chat.Chat chat, ChatState newState) { ChatState lastChatState = chatStates.get(chat); if (lastChatState != newState) { chatStates.put(chat, newState); @@ -142,7 +142,7 @@ public final class ChatStateManager extends Manager { return false; } - private static void fireNewChatState(Chat chat, ChatState state, Message message) { + private static void fireNewChatState(org.jivesoftware.smack.chat.Chat chat, ChatState state, Message message) { for (ChatMessageListener listener : chat.getListeners()) { if (listener instanceof ChatStateListener) { ((ChatStateListener) listener).stateChanged(chat, state, message); @@ -154,7 +154,7 @@ public final class ChatStateManager extends Manager { @Override public void processMessage(Message message) { - Chat chat = chatManager.getThreadChat(message.getThread()); + org.jivesoftware.smack.chat.Chat chat = chatManager.getThreadChat(message.getThread()); if (chat == null) { return; } @@ -166,11 +166,11 @@ public final class ChatStateManager extends Manager { private class IncomingMessageInterceptor implements ChatManagerListener, ChatMessageListener { - public void chatCreated(final Chat chat, boolean createdLocally) { + public void chatCreated(final org.jivesoftware.smack.chat.Chat chat, boolean createdLocally) { chat.addMessageListener(this); } - public void processMessage(Chat chat, Message message) { + public void processMessage(org.jivesoftware.smack.chat.Chat chat, Message message) { ExtensionElement extension = message.getExtension(NAMESPACE); if (extension == null) { return; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index d4d0d1e84..b47cc311d 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -37,8 +37,6 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; -import org.jivesoftware.smack.chat.Chat; -import org.jivesoftware.smack.chat.ChatManager; import org.jivesoftware.smack.chat.ChatMessageListener; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.FromMatchesFilter; @@ -1847,8 +1845,11 @@ public class MultiUserChat { * created chat. * @return new Chat for sending private messages to a given room occupant. */ - public Chat createPrivateChat(EntityFullJid occupant, ChatMessageListener listener) { - return ChatManager.getInstanceFor(connection).createChat(occupant, listener); + // TODO This should be made new not using chat.Chat. Private MUC chats are different from XMPP-IM 1:1 chats in to many ways. + // API sketch: PrivateMucChat createPrivateChat(Resourcepart nick) + @SuppressWarnings("deprecation") + public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityFullJid occupant, ChatMessageListener listener) { + return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener); } /** diff --git a/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java b/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java index 0a463b261..5a9b6c97e 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/chat/Chat.java @@ -36,7 +36,9 @@ import java.util.concurrent.CopyOnWriteArraySet; * sender. * * @author Matt Tucker + * @deprecated use org.jivesoftware.smack.chat2.Chat from smack-extensions instead. */ +@Deprecated public class Chat { private ChatManager chatManager; diff --git a/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManager.java b/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManager.java index 683a59e63..5c59136e9 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManager.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManager.java @@ -52,7 +52,9 @@ import org.jxmpp.jid.EntityJid; * made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}. * * @author Alexander Wenckus + * @deprecated use org.jivesoftware.smack.chat2.ChatManager from smack-extensions instead. */ +@Deprecated public final class ChatManager extends Manager{ private static final Logger LOGGER = Logger.getLogger(ChatManager.class.getName()); diff --git a/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManagerListener.java b/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManagerListener.java index eaa706810..73b12cdc9 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManagerListener.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatManagerListener.java @@ -30,5 +30,6 @@ public interface ChatManagerListener { * @param chat the chat that was created. * @param createdLocally true if the chat was created by the local user and false if it wasn't. */ + @SuppressWarnings("deprecation") void chatCreated(Chat chat, boolean createdLocally); } diff --git a/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatMessageListener.java b/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatMessageListener.java index 4ca62bbb0..a0396540f 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatMessageListener.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/chat/ChatMessageListener.java @@ -20,5 +20,6 @@ package org.jivesoftware.smack.chat; import org.jivesoftware.smack.packet.Message; public interface ChatMessageListener { + @SuppressWarnings("deprecation") void processMessage(Chat chat, Message message); } diff --git a/smack-im/src/test/java/org/jivesoftware/smack/chat/ChatConnectionTest.java b/smack-im/src/test/java/org/jivesoftware/smack/chat/ChatConnectionTest.java index 1fe9004b3..1c4d8a950 100644 --- a/smack-im/src/test/java/org/jivesoftware/smack/chat/ChatConnectionTest.java +++ b/smack-im/src/test/java/org/jivesoftware/smack/chat/ChatConnectionTest.java @@ -24,7 +24,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import org.jivesoftware.smack.DummyConnection; -import org.jivesoftware.smack.chat.ChatManager.MatchMode; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message.Type; import org.jivesoftware.smack.packet.Stanza; @@ -35,6 +34,7 @@ import org.junit.Test; import org.jxmpp.jid.Jid; import org.jxmpp.jid.JidTestUtil; +@SuppressWarnings("deprecation") public class ChatConnectionTest { private DummyConnection dc; @@ -46,7 +46,7 @@ public class ChatConnectionTest { public void setUp() throws Exception { // Defaults ChatManager.setDefaultIsNormalIncluded(true); - ChatManager.setDefaultMatchMode(MatchMode.BARE_JID); + ChatManager.setDefaultMatchMode(ChatManager.MatchMode.BARE_JID); dc = DummyConnection.newConnectedDummyConnection(); cm = ChatManager.getInstanceFor(dc); @@ -77,14 +77,14 @@ public class ChatConnectionTest { @Test public void validateDefaultSetMatchModeNone() { - ChatManager.setDefaultMatchMode(MatchMode.NONE); - assertEquals(MatchMode.NONE, ChatManager.getInstanceFor(new DummyConnection()).getMatchMode()); + ChatManager.setDefaultMatchMode(ChatManager.MatchMode.NONE); + assertEquals(ChatManager.MatchMode.NONE, ChatManager.getInstanceFor(new DummyConnection()).getMatchMode()); } @Test public void validateDefaultSetMatchModeEntityBareJid() { - ChatManager.setDefaultMatchMode(MatchMode.BARE_JID); - assertEquals(MatchMode.BARE_JID, ChatManager.getInstanceFor(new DummyConnection()).getMatchMode()); + ChatManager.setDefaultMatchMode(ChatManager.MatchMode.BARE_JID); + assertEquals(ChatManager.MatchMode.BARE_JID, ChatManager.getInstanceFor(new DummyConnection()).getMatchMode()); } @Test @@ -138,7 +138,7 @@ public class ChatConnectionTest { // No thread behaviour @Test public void chatMatchedOnJIDWhenNoThreadBareMode() { - // MatchMode.BARE_JID is the default, so setting required. + // ChatManager.MatchMode.BARE_JID is the default, so setting required. TestMessageListener msgListener = new TestMessageListener(); TestChatManagerListener listener = new TestChatManagerListener(msgListener); cm.addChatListener(listener); @@ -162,7 +162,7 @@ public class ChatConnectionTest { public void chatMatchedOnJIDWhenNoThreadJidMode() { TestMessageListener msgListener = new TestMessageListener(); TestChatManagerListener listener = new TestChatManagerListener(msgListener); - cm.setMatchMode(MatchMode.SUPPLIED_JID); + cm.setMatchMode(ChatManager.MatchMode.SUPPLIED_JID); cm.addChatListener(listener); Stanza incomingChat = createChatPacket(null, true); processServerMessage(incomingChat); @@ -188,7 +188,7 @@ public class ChatConnectionTest { public void chatMatchedOnJIDWhenNoThreadNoneMode() { TestMessageListener msgListener = new TestMessageListener(); TestChatManagerListener listener = new TestChatManagerListener(msgListener); - cm.setMatchMode(MatchMode.NONE); + cm.setMatchMode(ChatManager.MatchMode.NONE); cm.addChatListener(listener); Stanza incomingChat = createChatPacket(null, true); processServerMessage(incomingChat); diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java index 1a79e0a7b..6d068d88a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smack/ChatTest.java @@ -31,8 +31,6 @@ import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.jivesoftware.smack.SmackException.NotConnectedException; -import org.jivesoftware.smack.chat.Chat; -import org.jivesoftware.smack.chat.ChatManager; import org.jivesoftware.smack.chat.ChatManagerListener; import org.jivesoftware.smack.filter.ThreadFilter; import org.jivesoftware.smack.packet.Message; @@ -46,12 +44,14 @@ import org.jxmpp.stringprep.XmppStringprepException; */ public class ChatTest extends AbstractSmackIntegrationTest { - private final ChatManager chatManagerOne; + @SuppressWarnings("deprecation") + private final org.jivesoftware.smack.chat.ChatManager chatManagerOne; private boolean invoked; + @SuppressWarnings("deprecation") public ChatTest(SmackIntegrationTestEnvironment environment) { super(environment); - chatManagerOne = ChatManager.getInstanceFor(conOne); + chatManagerOne = org.jivesoftware.smack.chat.ChatManager.getInstanceFor(conOne); } @BeforeClass @@ -64,9 +64,10 @@ public class ChatTest extends AbstractSmackIntegrationTest { JivePropertiesManager.setJavaObjectEnabled(false); } + @SuppressWarnings("deprecation") @SmackIntegrationTest public void testProperties() throws XmppStringprepException, NotConnectedException, Exception { - Chat newChat = chatManagerOne.createChat(conTwo.getUser()); + org.jivesoftware.smack.chat.Chat newChat = chatManagerOne.createChat(conTwo.getUser()); StanzaCollector collector = conTwo.createStanzaCollector(new ThreadFilter(newChat.getThreadID())); Message msg = new Message(); @@ -112,12 +113,13 @@ public class ChatTest extends AbstractSmackIntegrationTest { getProperty(msg2, "birthdate")); } + @SuppressWarnings("deprecation") @SmackIntegrationTest public void chatManagerTest() { ChatManagerListener listener = new ChatManagerListener() { @Override - public void chatCreated(Chat chat, boolean createdLocally) { + public void chatCreated(org.jivesoftware.smack.chat.Chat chat, boolean createdLocally) { invoked = true; }