Smack/smack-experimental/src/main/java/org/jivesoftware/smackx/message_fastening/element/FasteningElement.java

328 lines
12 KiB
Java

/**
*
* 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.XmlElement;
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<XmlElement> wrappedPayloads = new ArrayList<>();
private final boolean clear;
private final boolean shell;
private FasteningElement(OriginIdElement originId,
List<XmlElement> 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<XmlElement> 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 (XmlElement 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<XmlElement> 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(XmlElement 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<XmlElement> 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.");
}
}
}
}