From 036ab79ada75c2d7d2a78e0127507edc10dccef5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 18 Jul 2020 12:55:26 +0200 Subject: [PATCH] Improved Support: XEP-0249: Direct MUC Invitations --- documentation/extensions/index.md | 2 +- documentation/extensions/invitation.md | 1 + .../muc/DirectMucInvitationListener.java | 25 +++ .../muc/DirectMucInvitationManager.java | 115 ++++++++++++ .../smackx/muc/MultiUserChatManager.java | 4 + .../muc/packet/GroupChatInvitation.java | 166 ++++++++++++------ .../provider/GroupChatInvitationProvider.java | 53 ++++++ .../extensions.providers | 2 +- .../GroupChatInvitationElementTest.java | 66 +++++++ 9 files changed, 382 insertions(+), 52 deletions(-) create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationListener.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationManager.java create mode 100644 smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java create mode 100644 smack-extensions/src/test/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitationElementTest.java diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 78a2bbc57..f26793168 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -84,12 +84,12 @@ Smack Extensions and currently supported XEPs of smack-extensions | Attention | [XEP-0224](https://xmpp.org/extensions/xep-0224.html) | n/a | Getting attention of another user. | | Bits of Binary | [XEP-0231](https://xmpp.org/extensions/xep-0231.html) | n/a | Including or referring to small bits of binary data in an XML stanza. | | Software Information | [XEP-0232](https://xmpp.org/extensions/xep-0232.html) | 0.3 | Allows an entity to provide detailed data about itself in Service Discovery responses. | +| Direct MUC Invitations | [XEP-0249](https://xmpp.org/extensions/xep-0249.html) | 1.2 | Invite other users to group chats. | | Best Practices for Resource Locking | [XEP-0296](https://xmpp.org/extensions/xep-0296.html) | n/a | Specifies best practices to be followed by Jabber/XMPP clients about when to lock into, and unlock away from, resources. | | Stanza Forwarding | [XEP-0297](https://xmpp.org/extensions/xep-0297.html) | n/a | Allows forwarding of Stanzas. | | Last Message Correction | [XEP-0308](https://xmpp.org/extensions/xep-0308.html) | n/a | Provides a method for indicating that a message is a correction of the last sent message. | | Last User Interaction in Presence | [XEP-0319](https://xmpp.org/extensions/xep-0319.html) | n/a | Communicate time of last user interaction via XMPP presence notifications. | | Data Forms Geolocation Element | [XEP-0350](https://xmpp.org/extensions/xep-0350.html) | n/a | Allows to include XEP-0080 gelocation data in XEP-0004 data forms. | -| [Group Chat Invitations](invitation.md) | n/a | n/a | Send invitations to other users to join a group chat room. | | [Jive Properties](properties.md) | n/a | n/a | TODO | diff --git a/documentation/extensions/invitation.md b/documentation/extensions/invitation.md index f940bb06f..f2ee1d529 100644 --- a/documentation/extensions/invitation.md +++ b/documentation/extensions/invitation.md @@ -10,6 +10,7 @@ group chat room. * Listen for Invitations **XEP related:** N/A -- this protocol is outdated now that the Multi-User Chat (MUC) XEP is available ([XEP-45](http://www.xmpp.org/extensions/xep-0045.html)). However, most existing clients still use this older protocol. Once MUC support becomes more widespread, this API may be deprecated. +**XEP related:** Now there is support for XEP-0249: Direct MUC Invitations. Inviting Other Users -------------------- diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationListener.java new file mode 100644 index 000000000..e8ef1af5e --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationListener.java @@ -0,0 +1,25 @@ +/** + * + * Copyright 2020 Paul Schaub. + * + * 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.smackx.muc; + +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.muc.packet.GroupChatInvitation; + +public interface DirectMucInvitationListener { + + void invitationReceived(GroupChatInvitation invitation, Stanza stanza); +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationManager.java new file mode 100644 index 000000000..24156a924 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/DirectMucInvitationManager.java @@ -0,0 +1,115 @@ +/** + * + * Copyright 2020 Paul Schaub. + * + * 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.smackx.muc; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.StanzaExtensionFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.MessageBuilder; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.muc.packet.GroupChatInvitation; + +import org.jxmpp.jid.EntityBareJid; + +/** + * Smacks API for XEP-0249: Direct MUC Invitations. + * Use this instead of {@link org.jivesoftware.smackx.muc.packet.MUCUser.Invite}. + * + * To invite a user to a group chat, use {@link #inviteToMuc(MultiUserChat, EntityBareJid)}. + * + * In order to listen for incoming invitations, register a {@link DirectMucInvitationListener} using + * {@link #addInvitationListener(DirectMucInvitationListener)}. + * + * @see Direct MUC Invitations + */ +public final class DirectMucInvitationManager extends Manager { + + private static final Map INSTANCES = new WeakHashMap<>(); + private final List directMucInvitationListeners = new ArrayList<>(); + private final ServiceDiscoveryManager serviceDiscoveryManager; + + static { + XMPPConnectionRegistry.addConnectionCreationListener(DirectMucInvitationManager::getInstanceFor); + } + + public static synchronized DirectMucInvitationManager getInstanceFor(XMPPConnection connection) { + DirectMucInvitationManager manager = INSTANCES.get(connection); + if (manager == null) { + manager = new DirectMucInvitationManager(connection); + INSTANCES.put(connection, manager); + } + return manager; + } + + private DirectMucInvitationManager(XMPPConnection connection) { + super(connection); + serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); + + registerExtensionElementListener(); + serviceDiscoveryManager.addFeature(GroupChatInvitation.NAMESPACE); + } + + private void registerExtensionElementListener() { + connection().addAsyncStanzaListener(stanza -> { + GroupChatInvitation invitation = stanza.getExtension(GroupChatInvitation.class); + for (DirectMucInvitationListener listener : directMucInvitationListeners) { + listener.invitationReceived(invitation, stanza); + } + }, new StanzaExtensionFilter(GroupChatInvitation.ELEMENT, GroupChatInvitation.NAMESPACE)); + } + + public void inviteToMuc(MultiUserChat muc, EntityBareJid user) + throws SmackException.NotConnectedException, InterruptedException { + inviteToMuc(muc, user, null, null, false, null); + } + + public void inviteToMuc(MultiUserChat muc, EntityBareJid user, String password, String reason, boolean _continue, String thread) + throws SmackException.NotConnectedException, InterruptedException { + inviteToMuc(user, new GroupChatInvitation(muc.getRoom(), password, reason, _continue, thread)); + } + + public void inviteToMuc(EntityBareJid jid, GroupChatInvitation invitation) throws SmackException.NotConnectedException, InterruptedException { + Message invitationMessage = MessageBuilder.buildMessage() + .to(jid) + .addExtension(invitation) + .build(); + connection().sendStanza(invitationMessage); + } + + public boolean userSupportsInvitations(EntityBareJid jid) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, + SmackException.NoResponseException { + return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, GroupChatInvitation.NAMESPACE); + } + + public synchronized void addInvitationListener(DirectMucInvitationListener listener) { + this.directMucInvitationListeners.add(listener); + } + + public synchronized void removeInvitationListener(DirectMucInvitationListener listener) { + this.directMucInvitationListeners.remove(listener); + } +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java index 7d92e9817..738a7c89c 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChatManager.java @@ -79,6 +79,10 @@ import org.jxmpp.util.cache.ExpirationCache; * further attempts will be made for the other rooms. *

* + * Note: + * For inviting other users to a group chat or listening for such invitations, take a look at the + * {@link DirectMucInvitationManager} which provides an implementation of XEP-0249: Direct MUC Invitations. + * * @see XEP-0045: Multi-User Chat */ public final class MultiUserChatManager extends Manager { diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java index 135651b9a..ddec2dede 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitation.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2007 Jive Software. + * Copyright 2003-2007 Jive Software, 2020 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,46 +17,28 @@ package org.jivesoftware.smackx.muc.packet; -import java.io.IOException; - import javax.xml.namespace.QName; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.XmlEnvironment; -import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.EqualsUtil; +import org.jivesoftware.smack.util.HashCode; +import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jivesoftware.smack.xml.XmlPullParser; -import org.jivesoftware.smack.xml.XmlPullParserException; + +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; /** * A group chat invitation stanza extension, which is used to invite other - * users to a group chat room. To invite a user to a group chat room, address - * a new message to the user and set the room name appropriately, as in the - * following code example: + * users to a group chat room. * - *
- * Message message = new Message("user@chat.example.com");
- * message.setBody("Join me for a group chat!");
- * message.addExtension(new GroupChatInvitation("room@chat.example.com"););
- * con.sendStanza(message);
- * 
- * - * To listen for group chat invitations, use a StanzaExtensionFilter for the - * x element name and jabber:x:conference namespace, as in the - * following code example: - * - *
- * PacketFilter filter = new StanzaExtensionFilter("x", "jabber:x:conference");
- * // Create a stanza collector or stanza listeners using the filter...
- * 
- * - * Note: this protocol is outdated now that the Multi-User Chat (MUC) XEP is available - * (XEP-45). However, most - * existing clients still use this older protocol. Once MUC support becomes more - * widespread, this API may be deprecated. + * This implementation now conforms to XEP-0249: Direct MUC Invitations, + * while staying backwards compatible to legacy MUC invitations. * * @author Matt Tucker + * @author Paul Schaub */ public class GroupChatInvitation implements ExtensionElement { @@ -69,10 +51,19 @@ public class GroupChatInvitation implements ExtensionElement { * Namespace of the stanza extension. */ public static final String NAMESPACE = "jabber:x:conference"; - public static final QName QNAME = new QName(NAMESPACE, ELEMENT); - private final String roomAddress; + public static final String ATTR_CONTINUE = "continue"; + public static final String ATTR_JID = "jid"; + public static final String ATTR_PASSWORD = "password"; + public static final String ATTR_REASON = "reason"; + public static final String ATTR_THREAD = "thread"; + + private final EntityBareJid roomAddress; + private final String password; + private final String reason; + private final boolean _continue; + private final String thread; /** * Creates a new group chat invitation to the specified room address. @@ -81,9 +72,23 @@ public class GroupChatInvitation implements ExtensionElement { * chat.example.com. * * @param roomAddress the address of the group chat room. + * @deprecated use {@link #GroupChatInvitation(EntityBareJid)} instead. */ + @Deprecated public GroupChatInvitation(String roomAddress) { - this.roomAddress = roomAddress; + this(JidCreate.entityBareFromOrThrowUnchecked(roomAddress)); + } + + public GroupChatInvitation(EntityBareJid roomAddress) { + this(roomAddress, null, null, false, null); + } + + public GroupChatInvitation(EntityBareJid mucJid, String password, String reason, boolean _continue, String thread) { + this.roomAddress = Objects.requireNonNull(mucJid); + this.password = password; + this.reason = reason; + this._continue = _continue; + this.thread = thread; } /** @@ -91,12 +96,62 @@ public class GroupChatInvitation implements ExtensionElement { * are in the form room@service, where service is * the name of group chat server, such as chat.example.com. * + * TODO: Remove in Smack 4.5 + * @deprecated use {@link #getRoomAddressJid()} instead. * @return the address of the group chat room. */ + @Deprecated public String getRoomAddress() { + return roomAddress.asEntityBareJidString(); + } + + /** + * Returns the address of the group chat room as an {@link EntityBareJid}. + * + * @return room address + */ + public EntityBareJid getRoomAddressJid() { return roomAddress; } + /** + * Returns the password which is used to join the room. + * This value can be null if no password is required. + * + * @return password + */ + public String getPassword() { + return password; + } + + /** + * Return the reason of invitation. + * + * @return reason + */ + public String getReason() { + return reason; + } + + /** + * Returns true if the invitation represents the continuation of a one-to-one chat. + * The chat continues the thread returned by {@link #getThread()}. + * + * @return true if this is a continued one-to-one chat, false otherwise. + */ + public boolean isContinue() { + return _continue; + } + + /** + * In case of a continuation, this returns the thread name of the one-to-one chat that is being continued. + * + * @return thread + */ + public String getThread() { + return thread; + } + @Override public String getElementName() { return ELEMENT; @@ -108,11 +163,35 @@ public class GroupChatInvitation implements ExtensionElement { } @Override - public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { - XmlStringBuilder xml = new XmlStringBuilder(this); - xml.attribute("jid", getRoomAddress()); - xml.closeEmptyElement(); - return xml; + public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { + return new XmlStringBuilder(this) + .optBooleanAttribute(ATTR_CONTINUE, isContinue()) + .attribute(ATTR_JID, getRoomAddressJid()) + .optAttribute(ATTR_PASSWORD, getPassword()) + .optAttribute(ATTR_REASON, getReason()) + .optAttribute(ATTR_THREAD, getThread()) + .closeEmptyElement(); + } + + @Override + public boolean equals(Object obj) { + return EqualsUtil.equals(this, obj, (equalsBuilder, other) -> equalsBuilder + .append(getRoomAddressJid(), other.getRoomAddressJid()) + .append(getPassword(), other.getPassword()) + .append(getReason(), other.getReason()) + .append(isContinue(), other.isContinue()) + .append(getThread(), other.getThread())); + } + + @Override + public int hashCode() { + return HashCode.builder() + .append(getRoomAddressJid()) + .append(getPassword()) + .append(getReason()) + .append(isContinue()) + .append(getThread()) + .build(); } /** @@ -123,17 +202,4 @@ public class GroupChatInvitation implements ExtensionElement { public static GroupChatInvitation from(Stanza packet) { return packet.getExtension(GroupChatInvitation.class); } - - public static class Provider extends ExtensionElementProvider { - - @Override - public GroupChatInvitation parse(XmlPullParser parser, - int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, - IOException { - String roomAddress = parser.getAttributeValue("", "jid"); - // Advance to end of extension. - parser.next(); - return new GroupChatInvitation(roomAddress); - } - } } diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java new file mode 100644 index 000000000..17db1a43f --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/provider/GroupChatInvitationProvider.java @@ -0,0 +1,53 @@ +/** + * + * Copyright 2020 Paul Schaub. + * + * 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.smackx.muc.provider; + +import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_CONTINUE; +import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_JID; +import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_PASSWORD; +import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_REASON; +import static org.jivesoftware.smackx.muc.packet.GroupChatInvitation.ATTR_THREAD; + +import java.io.IOException; + +import org.jivesoftware.smack.packet.XmlEnvironment; +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.muc.packet.GroupChatInvitation; + +import org.jxmpp.jid.EntityBareJid; + +public class GroupChatInvitationProvider extends ExtensionElementProvider { + + public static final GroupChatInvitationProvider TEST_PROVIDER = new GroupChatInvitationProvider(); + + @Override + public GroupChatInvitation parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) + throws XmlPullParserException, IOException, SmackParsingException { + + EntityBareJid roomJid = ParserUtils.getBareJidAttribute(parser, ATTR_JID); + String password = parser.getAttributeValue(ATTR_PASSWORD); + String reason = parser.getAttributeValue(ATTR_REASON); + boolean isContinue = ParserUtils.getBooleanAttribute(parser, ATTR_CONTINUE, false); + String thread = parser.getAttributeValue(ATTR_THREAD); + + return new GroupChatInvitation(roomJid, password, reason, isContinue, thread); + } +} diff --git a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers index 6d7ebc38c..48b5a2094 100644 --- a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers +++ b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers @@ -510,7 +510,7 @@ x jabber:x:conference - org.jivesoftware.smackx.muc.packet.GroupChatInvitation$Provider + org.jivesoftware.smackx.muc.provider.GroupChatInvitationProvider diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitationElementTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitationElementTest.java new file mode 100644 index 000000000..00850ac54 --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/muc/packet/GroupChatInvitationElementTest.java @@ -0,0 +1,66 @@ +/** + * + * Copyright 2020 Paul Schaub. + * + * 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.smackx.muc.packet; + +import static org.jivesoftware.smack.test.util.XmlAssertUtil.assertXmlSimilar; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.jivesoftware.smack.parsing.SmackParsingException; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smack.xml.XmlPullParserException; +import org.jivesoftware.smackx.muc.provider.GroupChatInvitationProvider; + +import org.junit.jupiter.api.Test; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; + +public class GroupChatInvitationElementTest { + + private static final EntityBareJid mucJid = JidCreate.entityBareFromOrThrowUnchecked("darkcave@macbeth.shakespeare.lit"); + + @Test + public void serializeFullElement() throws XmlPullParserException, IOException, SmackParsingException { + final String expectedXml = "" + + ""; + + GroupChatInvitation invitation = new GroupChatInvitation(mucJid, "cauldronburn", + "Hey Hecate, this is the place for all good witches!", true, + "e0ffe42b28561960c6b12b944a092794b9683a38"); + assertXmlSimilar(expectedXml, invitation.toXML()); + + GroupChatInvitation parsed = GroupChatInvitationProvider.TEST_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(invitation, parsed); + } + + @Test + public void serializeMinimalElementTest() throws XmlPullParserException, IOException, SmackParsingException { + final String expectedXml = ""; + + GroupChatInvitation invitation = new GroupChatInvitation(mucJid); + assertXmlSimilar(expectedXml, invitation.toXML()); + + GroupChatInvitation parsed = GroupChatInvitationProvider.TEST_PROVIDER.parse(TestUtils.getParser(expectedXml)); + assertEquals(invitation, parsed); + } +}