mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-12-23 11:07:57 +01:00
Merge pull request #355 from vanitasvitae/messageFastening
Message fastening
This commit is contained in:
commit
4f3d89e666
12 changed files with 929 additions and 0 deletions
|
@ -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
|
||||
--------------------------
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue