001/**
002 *
003 * Copyright 2020 Paul Schaub
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.fallback_indication;
018
019import java.util.Map;
020import java.util.Set;
021import java.util.WeakHashMap;
022import java.util.concurrent.CopyOnWriteArraySet;
023
024import org.jivesoftware.smack.AsyncButOrdered;
025import org.jivesoftware.smack.ConnectionCreationListener;
026import org.jivesoftware.smack.Manager;
027import org.jivesoftware.smack.SmackException;
028import org.jivesoftware.smack.XMPPConnection;
029import org.jivesoftware.smack.XMPPConnectionRegistry;
030import org.jivesoftware.smack.XMPPException;
031import org.jivesoftware.smack.filter.AndFilter;
032import org.jivesoftware.smack.filter.StanzaExtensionFilter;
033import org.jivesoftware.smack.filter.StanzaFilter;
034import org.jivesoftware.smack.filter.StanzaTypeFilter;
035import org.jivesoftware.smack.packet.Message;
036import org.jivesoftware.smack.packet.MessageBuilder;
037import org.jivesoftware.smack.packet.Stanza;
038import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
039import org.jivesoftware.smackx.fallback_indication.element.FallbackIndicationElement;
040
041import org.jxmpp.jid.BareJid;
042import org.jxmpp.jid.EntityBareJid;
043
044/**
045 * Smacks API for XEP-0428: Fallback Indication.
046 * In some scenarios it might make sense to mark the body of a message as fallback for legacy clients.
047 * Examples are encryption mechanisms where the sender might include a hint for legacy clients stating that the
048 * body (eg. "This message is encrypted") should be ignored.
049 *
050 * @see <a href="https://xmpp.org/extensions/xep-0428.html">XEP-0428: Fallback Indication</a>
051 */
052public final class FallbackIndicationManager extends Manager {
053
054    private static final Map<XMPPConnection, FallbackIndicationManager> INSTANCES = new WeakHashMap<>();
055
056    static {
057        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
058            @Override
059            public void connectionCreated(XMPPConnection connection) {
060                getInstanceFor(connection);
061            }
062        });
063    }
064
065    private final Set<FallbackIndicationListener> listeners = new CopyOnWriteArraySet<>();
066    private final AsyncButOrdered<BareJid> asyncButOrdered = new AsyncButOrdered<>();
067    private final StanzaFilter fallbackIndicationElementFilter = new AndFilter(StanzaTypeFilter.MESSAGE,
068            new StanzaExtensionFilter(FallbackIndicationElement.ELEMENT, FallbackIndicationElement.NAMESPACE));
069
070    private void fallbackIndicationElementListener(Stanza packet) {
071        Message message = (Message) packet;
072        FallbackIndicationElement indicator = FallbackIndicationElement.fromMessage(message);
073        String body = message.getBody();
074        asyncButOrdered.performAsyncButOrdered(message.getFrom().asBareJid(), () -> {
075            for (FallbackIndicationListener l : listeners) {
076                l.onFallbackIndicationReceived(message, indicator, body);
077            }
078        });
079    }
080
081    private FallbackIndicationManager(XMPPConnection connection) {
082        super(connection);
083        connection.addAsyncStanzaListener(this::fallbackIndicationElementListener, fallbackIndicationElementFilter);
084        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(FallbackIndicationElement.NAMESPACE);
085    }
086
087    public static synchronized FallbackIndicationManager getInstanceFor(XMPPConnection connection) {
088        FallbackIndicationManager manager = INSTANCES.get(connection);
089        if (manager == null) {
090            manager = new FallbackIndicationManager(connection);
091            INSTANCES.put(connection, manager);
092        }
093        return manager;
094    }
095
096    /**
097     * Determine, whether or not a user supports Fallback Indications.
098     *
099     * @param jid BareJid of the user.
100     * @return feature support
101     *
102     * @throws XMPPException.XMPPErrorException if a protocol level error happens
103     * @throws SmackException.NotConnectedException if the connection is not connected
104     * @throws InterruptedException if the thread is being interrupted
105     * @throws SmackException.NoResponseException if the server doesn't send a response in time
106     */
107    public boolean userSupportsFallbackIndications(EntityBareJid jid)
108            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
109            SmackException.NoResponseException {
110        return ServiceDiscoveryManager.getInstanceFor(connection())
111                .supportsFeature(jid, FallbackIndicationElement.NAMESPACE);
112    }
113
114    /**
115     * Determine, whether or not the server supports Fallback Indications.
116     *
117     * @return server side feature support
118     *
119     * @throws XMPPException.XMPPErrorException if a protocol level error happens
120     * @throws SmackException.NotConnectedException if the connection is not connected
121     * @throws InterruptedException if the thread is being interrupted
122     * @throws SmackException.NoResponseException if the server doesn't send a response in time
123     */
124    public boolean serverSupportsFallbackIndications()
125            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
126            SmackException.NoResponseException {
127        return ServiceDiscoveryManager.getInstanceFor(connection())
128                .serverSupportsFeature(FallbackIndicationElement.NAMESPACE);
129    }
130
131    /**
132     * Set the body of the message to the provided fallback message and add a {@link FallbackIndicationElement}.
133     *
134     * @param messageBuilder message builder
135     * @param fallbackMessageBody fallback message body
136     * @return builder with set body and added fallback element
137     */
138    public static MessageBuilder addFallbackIndicationWithBody(MessageBuilder messageBuilder, String fallbackMessageBody) {
139        return addFallbackIndication(messageBuilder).setBody(fallbackMessageBody);
140    }
141
142    /**
143     * Add a {@link FallbackIndicationElement} to the provided message builder.
144     *
145     * @param messageBuilder message builder
146     * @return message builder with added fallback element
147     */
148    public static MessageBuilder addFallbackIndication(MessageBuilder messageBuilder) {
149        return messageBuilder.addExtension(new FallbackIndicationElement());
150    }
151
152    /**
153     * Register a {@link FallbackIndicationListener} that gets notified whenever a message that contains a
154     * {@link FallbackIndicationElement} is received.
155     *
156     * @param listener listener to be registered.
157     */
158    public synchronized void addFallbackIndicationListener(FallbackIndicationListener listener) {
159        listeners.add(listener);
160    }
161
162    /**
163     * Unregister a {@link FallbackIndicationListener}.
164     *
165     * @param listener listener to be unregistered.
166     */
167    public synchronized void removeFallbackIndicationListener(FallbackIndicationListener listener) {
168        listeners.remove(listener);
169    }
170}