Add support for XEP-0422: Message Fastening

SMACK-884
This commit is contained in:
Paul Schaub 2020-04-13 18:17:26 +02:00
parent 72a9cb65a6
commit e0f7ddf5a8
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
11 changed files with 908 additions and 0 deletions

View File

@ -120,6 +120,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental
| [Consistent Color Generation](consistent_colors.md) | [XEP-0392](https://xmpp.org/extensions/xep-0392.html) | 0.6.0 | Generate consistent colors for identifiers like usernames to provide a consistent user experience. |
| [Message Markup](messagemarkup.md) | [XEP-0394](https://xmpp.org/extensions/xep-0394.html) | 0.1.0 | Style message bodies while keeping body and markup information separated. |
| DNS Queries over XMPP (DoX) | [XEP-0418](https://xmpp.org/extensions/xep-0418.html) | 0.1.0 | Send DNS queries and responses over XMPP. |
| Message Fastening | [XEP-0422](https://xmpp.org/extensions/xep-0422.html) | 0.1.1 | Mark payloads on a message to be logistically fastened to a previous message. |
Unofficial XMPP Extensions
--------------------------

View File

@ -0,0 +1,107 @@
/**
*
* Copyright 2019 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.message_fastening;
import java.util.List;
import java.util.WeakHashMap;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.packet.MessageBuilder;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.message_fastening.element.FasteningElement;
/**
* Smacks API for XEP-0422: Message Fastening.
* The API is still very bare bones, as the XEP intends Message Fastening to be used as a tool by other protocols.
*
* To enable / disable auto-announcing support for this feature, call {@link #setEnabledByDefault(boolean)} (default true).
*
* To fasten a payload to a previous message, create an {@link FasteningElement} using the builder provided by
* {@link FasteningElement#builder()}.
*
* You need to provide the {@link org.jivesoftware.smackx.sid.element.OriginIdElement} of the message you want to reference.
* Then add wrapped payloads using {@link FasteningElement.Builder#addWrappedPayloads(List)}
* and external payloads using {@link FasteningElement.Builder#addExternalPayloads(List)}.
*
* If you fastened some payloads onto the message previously and now want to replace the previous fastening, call
* {@link FasteningElement.Builder#isRemovingElement()}.
* Once you are finished, build the {@link FasteningElement} using {@link FasteningElement.Builder#build()} and add it to
* a stanza by calling {@link FasteningElement#applyTo(MessageBuilder)}.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html">XEP-0422: Message Fastening</a>
*/
public final class MessageFasteningManager extends Manager {
public static final String NAMESPACE = "urn:xmpp:fasten:0";
private static boolean ENABLED_BY_DEFAULT = true;
private static final WeakHashMap<XMPPConnection, MessageFasteningManager> INSTANCES = new WeakHashMap<>();
static {
XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
@Override
public void connectionCreated(XMPPConnection connection) {
if (ENABLED_BY_DEFAULT) {
MessageFasteningManager.getInstanceFor(connection).announceSupport();
}
}
});
}
private MessageFasteningManager(XMPPConnection connection) {
super(connection);
}
public static synchronized MessageFasteningManager getInstanceFor(XMPPConnection connection) {
MessageFasteningManager manager = INSTANCES.get(connection);
if (manager == null) {
manager = new MessageFasteningManager(connection);
INSTANCES.put(connection, manager);
}
return manager;
}
/**
* Enable or disable auto-announcing support for Message Fastening.
* Default is enabled.
*
* @param enabled enabled
*/
public static synchronized void setEnabledByDefault(boolean enabled) {
ENABLED_BY_DEFAULT = enabled;
}
/**
* Announce support for Message Fastening via Service Discovery.
*/
public void announceSupport() {
ServiceDiscoveryManager discoveryManager = ServiceDiscoveryManager.getInstanceFor(connection());
discoveryManager.addFeature(NAMESPACE);
}
/**
* Stop announcing support for Message Fastening.
*/
public void stopAnnouncingSupport() {
ServiceDiscoveryManager discoveryManager = ServiceDiscoveryManager.getInstanceFor(connection());
discoveryManager.removeFeature(NAMESPACE);
}
}

View File

@ -0,0 +1,84 @@
/**
*
* Copyright 2019 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.message_fastening.element;
import org.jivesoftware.smack.packet.NamedElement;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.util.XmlStringBuilder;
/**
* Child element of {@link FasteningElement}.
* Reference to a top level element in the stanza that contains the {@link FasteningElement}.
*/
public class ExternalElement implements NamedElement {
public static final String ELEMENT = "external";
public static final String ATTR_NAME = "name";
public static final String ATTR_ELEMENT_NAMESPACE = "element-namespace";
private final String name;
private final String elementNamespace;
/**
* Create a new {@link ExternalElement} that references a top level element with the given name.
*
* @param name name of the top level element
*/
public ExternalElement(String name) {
this(name, null);
}
/**
* Create a new {@link ExternalElement} that references a top level element with the given name and namespace.
*
* @param name name of the top level element
* @param elementNamespace namespace of the top level element
*/
public ExternalElement(String name, String elementNamespace) {
this.name = name;
this.elementNamespace = elementNamespace;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
XmlStringBuilder xml = new XmlStringBuilder(this);
xml.attribute(ATTR_NAME, getName());
xml.optAttribute(ATTR_ELEMENT_NAMESPACE, getElementNamespace());
return xml.closeEmptyElement();
}
/**
* Name of the referenced top level element, eg. 'body'.
* @return element name
*/
public String getName() {
return name;
}
/**
* Namespace of the referenced top level element, eg. 'urn:example:lik'.
* @return element namespace
*/
public String getElementNamespace() {
return elementNamespace;
}
}

View File

@ -0,0 +1,325 @@
/**
*
* Copyright 2019 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.message_fastening.element;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.MessageBuilder;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smackx.message_fastening.MessageFasteningManager;
import org.jivesoftware.smackx.sid.element.OriginIdElement;
/**
* Message Fastening container element.
*/
public final class FasteningElement implements ExtensionElement {
public static final String ELEMENT = "apply-to";
public static final String NAMESPACE = MessageFasteningManager.NAMESPACE;
public static final String ATTR_ID = "id";
public static final String ATTR_CLEAR = "clear";
public static final String ATTR_SHELL = "shell";
private final OriginIdElement referencedStanzasOriginId;
private final List<ExternalElement> externalPayloads = new ArrayList<>();
private final List<ExtensionElement> wrappedPayloads = new ArrayList<>();
private final boolean clear;
private final boolean shell;
private FasteningElement(OriginIdElement originId,
List<ExtensionElement> wrappedPayloads,
List<ExternalElement> externalPayloads,
boolean clear,
boolean shell) {
this.referencedStanzasOriginId = Objects.requireNonNull(originId, "Fastening element MUST contain an origin-id.");
this.wrappedPayloads.addAll(wrappedPayloads);
this.externalPayloads.addAll(externalPayloads);
this.clear = clear;
this.shell = shell;
}
/**
* Return the {@link OriginIdElement origin-id} of the {@link Stanza} that the message fastenings are to be
* applied to.
*
* @return origin id of the referenced stanza
*/
public OriginIdElement getReferencedStanzasOriginId() {
return referencedStanzasOriginId;
}
/**
* Return all wrapped payloads of this element.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#wrapped-payloads">XEP-0422: §3.1. Wrapped Payloads</a>
*
* @return wrapped payloads.
*/
public List<ExtensionElement> getWrappedPayloads() {
return Collections.unmodifiableList(wrappedPayloads);
}
/**
* Return all external payloads of this element.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#external-payloads">XEP-0422: §3.2. External Payloads</a>
*
* @return external payloads.
*/
public List<ExternalElement> getExternalPayloads() {
return Collections.unmodifiableList(externalPayloads);
}
/**
* Does this element remove a previously sent {@link FasteningElement}?
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#remove">
* XEP-0422: Message Fastening §3.4 Removing fastenings</a>
*
* @return true if the clear attribute is set.
*/
public boolean isRemovingElement() {
return clear;
}
/**
* Is this a shell element?
* Shell elements are otherwise empty elements that indicate that an encrypted payload of a message
* encrypted using XEP-420: Stanza Content Encryption contains a sensitive {@link FasteningElement}.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#encryption">
* XEP-0422: Message Fastening §3.5 Interaction with stanza encryption</a>
*
* @return true if this is a shell element.
*/
public boolean isShellElement() {
return shell;
}
/**
* Return true if the provided {@link Message} contains a {@link FasteningElement}.
*
* @param message message
* @return true if the stanza has an {@link FasteningElement}.
*/
public static boolean hasFasteningElement(Message message) {
return message.hasExtension(ELEMENT, MessageFasteningManager.NAMESPACE);
}
/**
* Return true if the provided {@link MessageBuilder} contains a {@link FasteningElement}.
*
* @param builder message builder
* @return true if the stanza has an {@link FasteningElement}.
*/
public static boolean hasFasteningElement(MessageBuilder builder) {
return builder.hasExtension(FasteningElement.class);
}
@Override
public String getNamespace() {
return MessageFasteningManager.NAMESPACE;
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
XmlStringBuilder xml = new XmlStringBuilder(this)
.attribute(ATTR_ID, referencedStanzasOriginId.getId())
.optBooleanAttribute(ATTR_CLEAR, isRemovingElement())
.optBooleanAttribute(ATTR_SHELL, isShellElement())
.rightAngleBracket();
addPayloads(xml);
return xml.closeElement(this);
}
private void addPayloads(XmlStringBuilder xml) {
for (ExternalElement external : externalPayloads) {
xml.append(external);
}
for (ExtensionElement wrapped : wrappedPayloads) {
xml.append(wrapped);
}
}
public static FasteningElement createShellElementForSensitiveElement(FasteningElement sensitiveElement) {
return createShellElementForSensitiveElement(sensitiveElement.getReferencedStanzasOriginId());
}
public static FasteningElement createShellElementForSensitiveElement(String originIdOfSensitiveElement) {
return createShellElementForSensitiveElement(new OriginIdElement(originIdOfSensitiveElement));
}
public static FasteningElement createShellElementForSensitiveElement(OriginIdElement originIdOfSensitiveElement) {
return FasteningElement.builder()
.setOriginId(originIdOfSensitiveElement)
.setShell()
.build();
}
/**
* Add this element to the provided message builder.
* Note: The stanza MUST NOT contain more than one apply-to elements at the same time.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#rules">XEP-0422 §4: Business Rules</a>
*
* @param messageBuilder message builder
*/
public void applyTo(MessageBuilder messageBuilder) {
if (FasteningElement.hasFasteningElement(messageBuilder)) {
throw new IllegalArgumentException("Stanza cannot contain more than one apply-to elements.");
} else {
messageBuilder.addExtension(this);
}
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private OriginIdElement originId;
private final List<ExtensionElement> wrappedPayloads = new ArrayList<>();
private final List<ExternalElement> externalPayloads = new ArrayList<>();
private boolean isClear = false;
private boolean isShell = false;
/**
* Set the origin-id of the referenced message.
*
* @param originIdString origin id as String
* @return builder instance
*/
public Builder setOriginId(String originIdString) {
return setOriginId(new OriginIdElement(originIdString));
}
/**
* Set the {@link OriginIdElement} of the referenced message.
*
* @param originId origin-id as element
* @return builder instance
*/
public Builder setOriginId(OriginIdElement originId) {
this.originId = originId;
return this;
}
/**
* Add a wrapped payload.
*
* @param wrappedPayload wrapped payload
* @return builder instance
*/
public Builder addWrappedPayload(ExtensionElement wrappedPayload) {
return addWrappedPayloads(Collections.singletonList(wrappedPayload));
}
/**
* Add multiple wrapped payloads at once.
*
* @param wrappedPayloads list of wrapped payloads
* @return builder instance
*/
public Builder addWrappedPayloads(List<ExtensionElement> wrappedPayloads) {
this.wrappedPayloads.addAll(wrappedPayloads);
return this;
}
/**
* Add an external payload.
*
* @param externalPayload external payload
* @return builder instance
*/
public Builder addExternalPayload(ExternalElement externalPayload) {
return addExternalPayloads(Collections.singletonList(externalPayload));
}
/**
* Add multiple external payloads at once.
*
* @param externalPayloads external payloads
* @return builder instance
*/
public Builder addExternalPayloads(List<ExternalElement> externalPayloads) {
this.externalPayloads.addAll(externalPayloads);
return this;
}
/**
* Declare this {@link FasteningElement} to remove previous fastenings.
* Semantically the wrapped payloads of this element declares all wrapped payloads from the referenced
* fastening element that share qualified names as removed.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#remove">
* XEP-0422: Message Fastening §3.4 Removing fastenings</a>
*
* @return builder instance
*/
public Builder setClear() {
isClear = true;
return this;
}
/**
* Declare this {@link FasteningElement} to be a shell element.
* Shell elements are used as hints that a Stanza Content Encryption payload contains another sensitive
* {@link FasteningElement}. The outer "shell" {@link FasteningElement} is used to do fastening collation.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#encryption">XEP-0422: Message Fastening §3.5 Interaction with stanza encryption</a>
* @see <a href="https://xmpp.org/extensions/xep-0420.html">XEP-0420: Stanza Content Encryption</a>
*
* @return builder instance
*/
public Builder setShell() {
isShell = true;
return this;
}
/**
* Build the element.
* @return built element.
*/
public FasteningElement build() {
validateThatIfIsShellThenOtherwiseEmpty();
return new FasteningElement(originId, wrappedPayloads, externalPayloads, isClear, isShell);
}
private void validateThatIfIsShellThenOtherwiseEmpty() {
if (!isShell) {
return;
}
if (isClear || !wrappedPayloads.isEmpty() || !externalPayloads.isEmpty()) {
throw new IllegalArgumentException("A fastening that is a shell element must be otherwise empty " +
"and cannot have a 'clear' attribute.");
}
}
}
}

View File

@ -0,0 +1,25 @@
/**
*
* Copyright 2019 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.
*/
/**
* XEP-0422: Message Fastening.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html">XEP-0422: Message
* Fastening</a>
*
*/
package org.jivesoftware.smackx.message_fastening.element;

View File

@ -0,0 +1,25 @@
/**
*
* Copyright 2019 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.
*/
/**
* XEP-0422: Message Fastening.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html">XEP-0422: Message
* Fastening</a>
*
*/
package org.jivesoftware.smackx.message_fastening;

View File

@ -0,0 +1,80 @@
/**
*
* Copyright 2019 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.message_fastening.provider;
import java.io.IOException;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.message_fastening.MessageFasteningManager;
import org.jivesoftware.smackx.message_fastening.element.ExternalElement;
import org.jivesoftware.smackx.message_fastening.element.FasteningElement;
public class FasteningElementProvider extends ExtensionElementProvider<FasteningElement> {
public static final FasteningElementProvider TEST_INSTANCE = new FasteningElementProvider();
@Override
public FasteningElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
FasteningElement.Builder builder = FasteningElement.builder();
builder.setOriginId(parser.getAttributeValue("", FasteningElement.ATTR_ID));
if (ParserUtils.getBooleanAttribute(parser, FasteningElement.ATTR_CLEAR, false)) {
builder.setClear();
}
if (ParserUtils.getBooleanAttribute(parser, FasteningElement.ATTR_SHELL, false)) {
builder.setShell();
}
outerloop: while (true) {
XmlPullParser.Event tag = parser.next();
switch (tag) {
case START_ELEMENT:
String name = parser.getName();
String namespace = parser.getNamespace();
// Parse external payload
if (MessageFasteningManager.NAMESPACE.equals(namespace) && ExternalElement.ELEMENT.equals(name)) {
ExternalElement external = new ExternalElement(
parser.getAttributeValue("", ExternalElement.ATTR_NAME),
parser.getAttributeValue("", ExternalElement.ATTR_ELEMENT_NAMESPACE));
builder.addExternalPayload(external);
continue;
}
// Parse wrapped payload
ExtensionElement wrappedPayload = PacketParserUtils.parseExtensionElement(name, namespace, parser, xmlEnvironment);
builder.addWrappedPayload(wrappedPayload);
break;
case END_ELEMENT:
if (parser.getDepth() == initialDepth) {
break outerloop;
}
break;
default:
break;
}
}
return builder.build();
}
}

View File

@ -0,0 +1,25 @@
/**
*
* Copyright 2019 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.
*/
/**
* XEP-0422: Message Fastening.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html">XEP-0422: Message
* Fastening</a>
*
*/
package org.jivesoftware.smackx.message_fastening.provider;

View File

@ -292,6 +292,12 @@
<className>org.jivesoftware.smackx.dox.provider.DnsIqProvider</className>
</iqProvider>
<!-- XEP-0422: Message Fastening -->
<extensionProvider>
<elementName>apply-to</elementName>
<namespace>urn:xmpp:fasten:0</namespace>
<className>org.jivesoftware.smackx.message_fastening.provider.FasteningElementProvider</className>
</extensionProvider>
<!-- XEP-xxxx: Multi-User Chat Light -->
<iqProvider>

View File

@ -8,5 +8,6 @@
<className>org.jivesoftware.smackx.eme.ExplicitMessageEncryptionManager</className>
<className>org.jivesoftware.smackx.sid.StableUniqueStanzaIdManager</className>
<className>org.jivesoftware.smackx.xmlelement.DataFormsXmlElementManager</className>
<className>org.jivesoftware.smackx.message_fastening.MessageFasteningManager</className>
</startupClasses>
</smack>

View File

@ -0,0 +1,229 @@
/**
*
* Copyright 2019 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.message_fastening;
import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.Arrays;
import org.jivesoftware.smack.packet.MessageBuilder;
import org.jivesoftware.smack.packet.StandardExtensionElement;
import org.jivesoftware.smack.packet.StanzaFactory;
import org.jivesoftware.smack.packet.id.StandardStanzaIdSource;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.test.util.SmackTestUtil;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.message_fastening.element.ExternalElement;
import org.jivesoftware.smackx.message_fastening.element.FasteningElement;
import org.jivesoftware.smackx.message_fastening.provider.FasteningElementProvider;
import org.jivesoftware.smackx.sid.element.OriginIdElement;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
public class MessageFasteningElementsTest {
private final StanzaFactory stanzaFactory = new StanzaFactory(new StandardStanzaIdSource());
/**
* Test XML serialization of the {@link FasteningElement} using the example provided by
* the XEP.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#wrapped-payloads">XEP-0422 §3.1 Wrapped Payloads</a>
*/
@Test
public void fasteningElementSerializationTest() {
String xml = "" +
"<apply-to xmlns='urn:xmpp:fasten:0' id='origin-id-1'>" +
" <i-like-this xmlns='urn:example:like'/>" +
"</apply-to>";
FasteningElement applyTo = FasteningElement.builder()
.setOriginId("origin-id-1")
.addWrappedPayload(new StandardExtensionElement("i-like-this", "urn:example:like"))
.build();
assertXmlSimilar(xml, applyTo.toXML().toString());
}
@ParameterizedTest
@EnumSource(SmackTestUtil.XmlPullParserKind.class)
public void fasteningDeserializationTest(SmackTestUtil.XmlPullParserKind parserKind) throws XmlPullParserException, IOException, SmackParsingException {
String xml = "" +
"<apply-to xmlns='urn:xmpp:fasten:0' id='origin-id-1'>" +
" <i-like-this xmlns='urn:example:like'/>" +
" <external name='custom' element-namespace='urn:example:custom'/>" +
" <external name='body'/>" +
"</apply-to>";
FasteningElement parsed = SmackTestUtil.parse(xml, FasteningElementProvider.class, parserKind);
assertNotNull(parsed);
assertEquals(new OriginIdElement("origin-id-1"), parsed.getReferencedStanzasOriginId());
assertFalse(parsed.isRemovingElement());
assertFalse(parsed.isShellElement());
assertEquals(1, parsed.getWrappedPayloads().size());
assertEquals("i-like-this", parsed.getWrappedPayloads().get(0).getElementName());
assertEquals("urn:example:like", parsed.getWrappedPayloads().get(0).getNamespace());
assertEquals(2, parsed.getExternalPayloads().size());
ExternalElement custom = parsed.getExternalPayloads().get(0);
assertEquals("custom", custom.getName());
assertEquals("urn:example:custom", custom.getElementNamespace());
ExternalElement body = parsed.getExternalPayloads().get(1);
assertEquals("body", body.getName());
assertNull(body.getElementNamespace());
}
@Test
public void fasteningDeserializationClearTest() throws XmlPullParserException, IOException, SmackParsingException {
String xml = "" +
"<apply-to xmlns='urn:xmpp:fasten:0' id='origin-id-1' clear='true'>" +
" <i-like-this xmlns='urn:example:like'/>" +
"</apply-to>";
FasteningElement parsed = FasteningElementProvider.TEST_INSTANCE.parse(TestUtils.getParser(xml));
assertTrue(parsed.isRemovingElement());
}
@Test
public void fasteningElementWithExternalElementsTest() {
String xml = "" +
"<apply-to xmlns='urn:xmpp:fasten:0' id='origin-id-2'>" +
" <external name='body'/>" +
" <external name='custom' element-namespace='urn:example:custom'/>" +
" <edit xmlns='urn:example.edit'/>" +
"</apply-to>";
FasteningElement element = FasteningElement.builder()
.setOriginId("origin-id-2")
.addExternalPayloads(Arrays.asList(
new ExternalElement("body"),
new ExternalElement("custom", "urn:example:custom")
))
.addWrappedPayload(
new StandardExtensionElement("edit", "urn:example.edit"))
.build();
assertXmlSimilar(xml, element.toXML().toString());
}
@Test
public void createShellElementSharesOriginIdTest() {
OriginIdElement originIdElement = new OriginIdElement("sensitive-stanza-1");
FasteningElement sensitiveFastening = FasteningElement.builder()
.setOriginId(originIdElement)
.build();
FasteningElement shellElement = FasteningElement.createShellElementForSensitiveElement(sensitiveFastening);
assertEquals(originIdElement, shellElement.getReferencedStanzasOriginId());
}
@Test
public void fasteningRemoveSerializationTest() {
String xml =
"<apply-to xmlns='urn:xmpp:fasten:0' id='origin-id-1' clear='true'>" +
" <i-like-this xmlns='urn:example:like'>Very much</i-like-this>" +
"</apply-to>";
FasteningElement element = FasteningElement.builder()
.setOriginId("origin-id-1")
.setClear()
.addWrappedPayload(StandardExtensionElement.builder("i-like-this", "urn:example:like")
.setText("Very much")
.build())
.build();
assertXmlSimilar(xml, element.toXML().toString());
}
@Test
public void hasFasteningElementTest() {
MessageBuilder messageBuilderWithFasteningElement = MessageBuilder.buildMessage()
.setBody("Hi!")
.addExtension(FasteningElement.builder().setOriginId("origin-id-1").build());
MessageBuilder messageBuilderWithoutFasteningElement = MessageBuilder.buildMessage()
.setBody("Ho!");
assertTrue(FasteningElement.hasFasteningElement(messageBuilderWithFasteningElement));
assertFalse(FasteningElement.hasFasteningElement(messageBuilderWithoutFasteningElement));
}
@Test
public void shellElementMustNotHaveClearAttributeTest() {
assertThrows(IllegalArgumentException.class, () ->
FasteningElement.builder()
.setShell()
.setClear()
.build());
}
@Test
public void shellElementMustNotContainAnyPayloads() {
assertThrows(IllegalArgumentException.class, () ->
FasteningElement.builder()
.setShell()
.addWrappedPayload(new StandardExtensionElement("edit", "urn:example.edit"))
.build());
assertThrows(IllegalArgumentException.class, () ->
FasteningElement.builder()
.setShell()
.addExternalPayload(new ExternalElement("body"))
.build());
}
@Test
public void ensureAddFasteningElementToStanzaWorks() {
MessageBuilder message = stanzaFactory.buildMessageStanza();
FasteningElement fasteningElement = FasteningElement.builder().setOriginId("another-apply-to").build();
// Adding only one element is allowed
fasteningElement.applyTo(message);
}
/**
* Ensure, that {@link FasteningElement#applyTo(MessageBuilder)}
* throws when trying to add an {@link FasteningElement} to a {@link MessageBuilder} that already contains one
* such element.
*
* @see <a href="https://xmpp.org/extensions/xep-0422.html#rules">XEP-0422: §4. Business Rules</a>
*/
@Test
public void ensureStanzaCanOnlyContainOneFasteningElement() {
MessageBuilder messageWithFastening = stanzaFactory.buildMessageStanza();
FasteningElement.builder().setOriginId("origin-id").build().applyTo(messageWithFastening);
// Adding a second fastening MUST result in exception
Assertions.assertThrows(IllegalArgumentException.class, () ->
FasteningElement.builder().setOriginId("another-apply-to").build()
.applyTo(messageWithFastening));
}
}