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