diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md
index 47100f37f..f84b5c025 100644
--- a/documentation/extensions/index.md
+++ b/documentation/extensions/index.md
@@ -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
--------------------------
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/MessageFasteningManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/MessageFasteningManager.java
new file mode 100644
index 000000000..ee73afae1
--- /dev/null
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/MessageFasteningManager.java
@@ -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 XEP-0422: Message Fastening
+ */
+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 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);
+ }
+}
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/ExternalElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/ExternalElement.java
new file mode 100644
index 000000000..2a83352db
--- /dev/null
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/ExternalElement.java
@@ -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;
+ }
+}
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java
new file mode 100644
index 000000000..5328bd0b2
--- /dev/null
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java
@@ -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 externalPayloads = new ArrayList<>();
+ private final List wrappedPayloads = new ArrayList<>();
+ private final boolean clear;
+ private final boolean shell;
+
+ private FasteningElement(OriginIdElement originId,
+ List wrappedPayloads,
+ List 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 XEP-0422: §3.1. Wrapped Payloads
+ *
+ * @return wrapped payloads.
+ */
+ public List getWrappedPayloads() {
+ return Collections.unmodifiableList(wrappedPayloads);
+ }
+
+ /**
+ * Return all external payloads of this element.
+ *
+ * @see XEP-0422: §3.2. External Payloads
+ *
+ * @return external payloads.
+ */
+ public List getExternalPayloads() {
+ return Collections.unmodifiableList(externalPayloads);
+ }
+
+ /**
+ * Does this element remove a previously sent {@link FasteningElement}?
+ *
+ * @see
+ * XEP-0422: Message Fastening §3.4 Removing fastenings
+ *
+ * @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
+ * XEP-0422: Message Fastening §3.5 Interaction with stanza encryption
+ *
+ * @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 XEP-0422 §4: Business Rules
+ *
+ * @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 wrappedPayloads = new ArrayList<>();
+ private final List 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 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 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
+ * XEP-0422: Message Fastening §3.4 Removing fastenings
+ *
+ * @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 XEP-0422: Message Fastening §3.5 Interaction with stanza encryption
+ * @see XEP-0420: Stanza Content Encryption
+ *
+ * @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.");
+ }
+ }
+ }
+}
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/package-info.java
new file mode 100644
index 000000000..8ae0915c7
--- /dev/null
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/package-info.java
@@ -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 XEP-0422: Message
+ * Fastening
+ *
+ */
+package org.jivesoftware.smackx.message_fastening.element;
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/package-info.java
new file mode 100644
index 000000000..90dba9915
--- /dev/null
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/package-info.java
@@ -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 XEP-0422: Message
+ * Fastening
+ *
+ */
+package org.jivesoftware.smackx.message_fastening;
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/FasteningElementProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/FasteningElementProvider.java
new file mode 100644
index 000000000..7005bda99
--- /dev/null
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/FasteningElementProvider.java
@@ -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 {
+
+ 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();
+ }
+}
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/package-info.java
new file mode 100644
index 000000000..cf2fcf1ff
--- /dev/null
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/provider/package-info.java
@@ -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 XEP-0422: Message
+ * Fastening
+ *
+ */
+package org.jivesoftware.smackx.message_fastening.provider;
diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java
index 585be47a6..b157b40a0 100644
--- a/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java
+++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/sid/element/OriginIdElement.java
@@ -101,4 +101,25 @@ public class OriginIdElement extends StableAndUniqueIdElement {
.attribute(ATTR_ID, getId())
.closeEmptyElement();
}
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (!(other instanceof OriginIdElement)) {
+ return false;
+ }
+
+ OriginIdElement otherId = (OriginIdElement) other;
+ return getId().equals(otherId.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
}
diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers
index 81c36ee64..d594578a4 100644
--- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers
+++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers
@@ -292,6 +292,12 @@
org.jivesoftware.smackx.dox.provider.DnsIqProvider
+
+
+ apply-to
+ urn:xmpp:fasten:0
+ org.jivesoftware.smackx.message_fastening.provider.FasteningElementProvider
+
diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml
index 347a698f3..6a4f6a759 100644
--- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml
+++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.xml
@@ -8,5 +8,6 @@
org.jivesoftware.smackx.eme.ExplicitMessageEncryptionManager
org.jivesoftware.smackx.sid.StableUniqueStanzaIdManager
org.jivesoftware.smackx.xmlelement.DataFormsXmlElementManager
+ org.jivesoftware.smackx.message_fastening.MessageFasteningManager
diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java
new file mode 100644
index 000000000..9764b0363
--- /dev/null
+++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_fastening/MessageFasteningElementsTest.java
@@ -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 XEP-0422 §3.1 Wrapped Payloads
+ */
+ @Test
+ public void fasteningElementSerializationTest() {
+ String xml = "" +
+ "" +
+ " " +
+ "";
+
+ 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 = "" +
+ "" +
+ " " +
+ " " +
+ " " +
+ "";
+
+ 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 = "" +
+ "" +
+ " " +
+ "";
+
+ FasteningElement parsed = FasteningElementProvider.TEST_INSTANCE.parse(TestUtils.getParser(xml));
+
+ assertTrue(parsed.isRemovingElement());
+ }
+
+ @Test
+ public void fasteningElementWithExternalElementsTest() {
+ String xml = "" +
+ "" +
+ " " +
+ " " +
+ " " +
+ "";
+
+ 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 =
+ "" +
+ " Very much" +
+ "";
+
+ 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 XEP-0422: §4. Business Rules
+ */
+ @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));
+ }
+}