StanzaBuilder.java

/**
 *
 * Copyright 2019-2021 Florian Schmaus
 *
 * 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.smack.packet;

import java.util.Collection;
import java.util.List;

import javax.xml.namespace.QName;

import org.jivesoftware.smack.packet.id.StanzaIdSource;
import org.jivesoftware.smack.util.Function;
import org.jivesoftware.smack.util.MultiMap;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.ToStringUtil;
import org.jivesoftware.smack.util.XmppElementUtil;

import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;

public abstract class StanzaBuilder<B extends StanzaBuilder<B>> implements StanzaView {

    final StanzaIdSource stanzaIdSource;
    final String stanzaId;

    Jid to;
    Jid from;

    StanzaError stanzaError;

    String language;

    MultiMap<QName, XmlElement> extensionElements = new MultiMap<>();

    protected StanzaBuilder(StanzaBuilder<?> other) {
        stanzaIdSource = other.stanzaIdSource;
        stanzaId = other.stanzaId;

        to = other.to;
        from = other.from;
        stanzaError = other.stanzaError;
        language = other.language;
        extensionElements = other.extensionElements.clone();
    }

    protected StanzaBuilder(StanzaIdSource stanzaIdSource) {
        this.stanzaIdSource = stanzaIdSource;
        this.stanzaId = null;
    }

    protected StanzaBuilder(String stanzaId) {
        this.stanzaIdSource = null;
        this.stanzaId = StringUtils.requireNullOrNotEmpty(stanzaId, "Stanza ID must not be the empty String");
    }

    protected StanzaBuilder(Stanza message, String stanzaId) {
        this(stanzaId);
        copyFromStanza(message);
    }

    protected StanzaBuilder(Stanza message, StanzaIdSource stanzaIdSource) {
        this(stanzaIdSource);
        copyFromStanza(message);
    }

    private void copyFromStanza(Stanza stanza) {
        to = stanza.getTo();
        from = stanza.getFrom();
        stanzaError = stanza.getError();
        language = stanza.getLanguage();

        extensionElements = stanza.cloneExtensionsMap();
    }

    /**
     * Set the recipent address of the stanza.
     *
     * @param to whoe the stanza is being sent to.
     * @return a reference to this builder.
     * @throws XmppStringprepException if the provided character sequence is not a valid XMPP address.
     * @see #to(Jid)
     */
    public final B to(CharSequence to) throws XmppStringprepException {
        return to(JidCreate.from(to));
    }

    /**
     * Sets who the stanza is being sent "to". The XMPP protocol often makes the "to" attribute optional, so it does not
     * always need to be set.
     *
     * @param to who the stanza is being sent to.
     * @return a reference to this builder.
     */
    public final B to(Jid to) {
        this.to = to;
        return getThis();
    }

    /**
     * Sets who the stanza is being sent "from".
     *
     * @param from who the stanza is being sent from.
     * @return a reference to this builder.
     * @throws XmppStringprepException if the provided character sequence is not a valid XMPP address.
     * @see #from(Jid)
     */
    public final B from(CharSequence from) throws XmppStringprepException {
        return from(JidCreate.from(from));
    }

    /**
     * Sets who the stanza is being sent "from". The XMPP protocol often makes the "from" attribute optional, so it does
     * not always need to be set.
     *
     * @param from who the stanza is being sent from.
     * @return a reference to this builder.
     */
    public final B from(Jid from) {
        this.from = from;
        return getThis();
    }

    /**
     * Sets the error for this stanza.
     *
     * @param stanzaError the error to associate with this stanza.
     * @return a reference to this builder.
     */
    public final B setError(StanzaError stanzaError) {
        this.stanzaError = stanzaError;
        return getThis();
    }

    /**
     * Sets the xml:lang for this stanza.
     *
     * @param language the xml:lang of this stanza.
     * @return a reference to this builder.
     */
    public final B setLanguage(String language) {
        this.language = language;
        return getThis();
    }

    public final B addExtension(XmlElement extensionElement) {
        QName key = extensionElement.getQName();
        extensionElements.put(key, extensionElement);
        return getThis();
    }

    public final B addOptExtensions(Collection<? extends XmlElement> extensionElements) {
        if (extensionElements == null) {
            return getThis();
        }

        return addExtensions(extensionElements);
    }

    public final B addExtensions(Collection<? extends XmlElement> extensionElements) {
        for (XmlElement extensionElement : extensionElements) {
            addExtension(extensionElement);
        }
        return getThis();
    }

    public final B overrideExtension(XmlElement extensionElement) {
        QName key = extensionElement.getQName();
        extensionElements.remove(key);
        extensionElements.put(key, extensionElement);
        return getThis();
    }

    public final B removeExtension(String elementName, String namespace) {
        QName key = new QName(namespace, elementName);
        extensionElements.remove(key);
        return getThis();
    }

    public final B removeExtension(ExtensionElement extension) {
        QName key = extension.getQName();
        List<XmlElement> list = extensionElements.getAll(key);
        list.remove(extension);
        return getThis();
    }

    public abstract Stanza build();

    public abstract B getThis();

    @Override
    public final String getStanzaId() {
        return stanzaId;
    }

    @Override
    public final Jid getTo() {
        return to;
    }

    @Override
    public final Jid getFrom() {
        return from;
    }

    @Override
    public final String getLanguage() {
        return language;
    }

    @Override
    public final StanzaError getError() {
        return stanzaError;
    }

    @Override
    public final XmlElement getExtension(QName qname) {
        return extensionElements.getFirst(qname);
    }

    @Override
    public final List<XmlElement> getExtensions() {
        return extensionElements.values();
    }

    @Override
    public final List<XmlElement> getExtensions(QName qname) {
        return extensionElements.getAll(qname);
    }

    @Override
    public final <E extends ExtensionElement> List<E> getExtensions(Class<E> extensionElementClass) {
        return XmppElementUtil.getElementsFrom(extensionElements, extensionElementClass);
    }

    public final boolean willBuildStanzaWithId() {
        return stanzaIdSource != null || StringUtils.isNotEmpty(stanzaId);
    }

    public final void throwIfNoStanzaId() {
        if (willBuildStanzaWithId()) {
            return;
        }
        throw new IllegalArgumentException(
                        "The builder will not build a stanza with an ID set, although it is required");
    }

    protected abstract void addStanzaSpecificAttributes(ToStringUtil.Builder builder);

    @Override
    public final String toString() {
        ToStringUtil.Builder builder = ToStringUtil.builderFor(getClass())
            .addValue("id", stanzaId)
            .addValue("from", from)
            .addValue("to", to)
            .addValue("language", language)
            .addValue("error", stanzaError)
            ;

        addStanzaSpecificAttributes(builder);

        builder.add("Extension Elements", extensionElements.values(), e -> {
            return e.getQName();
        });

        return builder.build();
    }

    public static MessageBuilder buildMessage() {
        return buildMessage(null);
    }

    public static MessageBuilder buildMessage(String stanzaId) {
        return new MessageBuilder(stanzaId);
    }

    public static MessageBuilder buildMessageFrom(Message message, String stanzaId) {
        return new MessageBuilder(message, stanzaId);
    }

    public static MessageBuilder buildMessageFrom(Message message, StanzaIdSource stanzaIdSource) {
        return new MessageBuilder(message, stanzaIdSource);
    }

    public static PresenceBuilder buildPresence() {
        return buildPresence(null);
    }

    public static PresenceBuilder buildPresence(String stanzaId) {
        return new PresenceBuilder(stanzaId);
    }

    public static PresenceBuilder buildPresenceFrom(Presence presence, String stanzaId) {
        return new PresenceBuilder(presence, stanzaId);
    }

    public static PresenceBuilder buildPresenceFrom(Presence presence, StanzaIdSource stanzaIdSource) {
        return new PresenceBuilder(presence, stanzaIdSource);
    }

    public static IqData buildIqData(String stanzaId) {
        return new IqData(stanzaId);
    }

    public static <SB extends StanzaBuilder<?>> SB buildResponse(StanzaView request, Function<SB, String> builderFromStanzaId) {
        SB responseBuilder = builderFromStanzaId.apply(request.getStanzaId());

        responseBuilder.to(request.getFrom())
            .from(request.getTo())
            ;

        return responseBuilder;
    }

}