1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2025-09-09 09:09:38 +02:00

Deprecate Chat API, introduce new Chat API

Also add (From|To)TypeFilter and update/fix the documentation in a few places.
This commit is contained in:
Florian Schmaus 2017-01-11 19:35:55 +01:00
parent b0fef6ffcb
commit d47463a533
22 changed files with 612 additions and 113 deletions

View file

@ -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;
}
}

View file

@ -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.
* <p>
* 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.
* </p>
*
* @see <a href="https://xmpp.org/extensions/xep-0296.html">XEP-0296: Best Practices for Resource Locking</a>
*/
public final class ChatManager extends Manager {
private static final Map<XMPPConnection, ChatManager> 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<EntityBareJid, Chat> chats = new ConcurrentHashMap<>();
private final Set<IncomingChatMessageListener> incomingListeners = new CopyOnWriteArraySet<>();
private final Set<OutgoingChatMessageListener> 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 <code>jid</code>.
*
* @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;
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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<Chat, ChatState> chatStates = new WeakHashMap<Chat, ChatState>();
private final Map<org.jivesoftware.smack.chat.Chat, ChatState> 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;

View file

@ -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);
}
/**