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;
038
039import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
040import org.jivesoftware.smackx.fallback_indication.element.FallbackIndicationElement;
041
042import org.jxmpp.jid.BareJid;
043import org.jxmpp.jid.EntityBareJid;
044
045/**
046 * Smacks API for XEP-0428: Fallback Indication.
047 * In some scenarios it might make sense to mark the body of a message as fallback for legacy clients.
048 * Examples are encryption mechanisms where the sender might include a hint for legacy clients stating that the
049 * body (eg. "This message is encrypted") should be ignored.
050 *
051 * @see <a href="https://xmpp.org/extensions/xep-0428.html">XEP-0428: Fallback Indication</a>
052 */
053public final class FallbackIndicationManager extends Manager {
054
055    private static final Map<XMPPConnection, FallbackIndicationManager> INSTANCES = new WeakHashMap<>();
056
057    static {
058        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
059            @Override
060            public void connectionCreated(XMPPConnection connection) {
061                getInstanceFor(connection);
062            }
063        });
064    }
065
066    private final Set<FallbackIndicationListener> listeners = new CopyOnWriteArraySet<>();
067    private final AsyncButOrdered<BareJid> asyncButOrdered = new AsyncButOrdered<>();
068    private final StanzaFilter fallbackIndicationElementFilter = new AndFilter(StanzaTypeFilter.MESSAGE,
069            new StanzaExtensionFilter(FallbackIndicationElement.ELEMENT, FallbackIndicationElement.NAMESPACE));
070
071    private void fallbackIndicationElementListener(Stanza packet) {
072        Message message = (Message) packet;
073        FallbackIndicationElement indicator = FallbackIndicationElement.fromMessage(message);
074        String body = message.getBody();
075        asyncButOrdered.performAsyncButOrdered(message.getFrom().asBareJid(), () -> {
076            for (FallbackIndicationListener l : listeners) {
077                l.onFallbackIndicationReceived(message, indicator, body);
078            }
079        });
080    }
081
082    private FallbackIndicationManager(XMPPConnection connection) {
083        super(connection);
084        connection.addAsyncStanzaListener(this::fallbackIndicationElementListener, fallbackIndicationElementFilter);
085        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(FallbackIndicationElement.NAMESPACE);
086    }
087
088    public static synchronized FallbackIndicationManager getInstanceFor(XMPPConnection connection) {
089        FallbackIndicationManager manager = INSTANCES.get(connection);
090        if (manager == null) {
091            manager = new FallbackIndicationManager(connection);
092            INSTANCES.put(connection, manager);
093        }
094        return manager;
095    }
096
097    /**
098     * Determine, whether or not a user supports Fallback Indications.
099     *
100     * @param jid BareJid of the user.
101     * @return feature support
102     *
103     * @throws XMPPException.XMPPErrorException if a protocol level error happens
104     * @throws SmackException.NotConnectedException if the connection is not connected
105     * @throws InterruptedException if the thread is being interrupted
106     * @throws SmackException.NoResponseException if the server doesn't send a response in time
107     */
108    public boolean userSupportsFallbackIndications(EntityBareJid jid)
109            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
110            SmackException.NoResponseException {
111        return ServiceDiscoveryManager.getInstanceFor(connection())
112                .supportsFeature(jid, FallbackIndicationElement.NAMESPACE);
113    }
114
115    /**
116     * Determine, whether or not the server supports Fallback Indications.
117     *
118     * @return server side feature support
119     *
120     * @throws XMPPException.XMPPErrorException if a protocol level error happens
121     * @throws SmackException.NotConnectedException if the connection is not connected
122     * @throws InterruptedException if the thread is being interrupted
123     * @throws SmackException.NoResponseException if the server doesn't send a response in time
124     */
125    public boolean serverSupportsFallbackIndications()
126            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
127            SmackException.NoResponseException {
128        return ServiceDiscoveryManager.getInstanceFor(connection())
129                .serverSupportsFeature(FallbackIndicationElement.NAMESPACE);
130    }
131
132    /**
133     * Set the body of the message to the provided fallback message and add a {@link FallbackIndicationElement}.
134     *
135     * @param messageBuilder message builder
136     * @param fallbackMessageBody fallback message body
137     * @return builder with set body and added fallback element
138     */
139    public static MessageBuilder addFallbackIndicationWithBody(MessageBuilder messageBuilder, String fallbackMessageBody) {
140        return addFallbackIndication(messageBuilder).setBody(fallbackMessageBody);
141    }
142
143    /**
144     * Add a {@link FallbackIndicationElement} to the provided message builder.
145     *
146     * @param messageBuilder message builder
147     * @return message builder with added fallback element
148     */
149    public static MessageBuilder addFallbackIndication(MessageBuilder messageBuilder) {
150        return messageBuilder.addExtension(new FallbackIndicationElement());
151    }
152
153    /**
154     * Register a {@link FallbackIndicationListener} that gets notified whenever a message that contains a
155     * {@link FallbackIndicationElement} is received.
156     *
157     * @param listener listener to be registered.
158     */
159    public synchronized void addFallbackIndicationListener(FallbackIndicationListener listener) {
160        listeners.add(listener);
161    }
162
163    /**
164     * Unregister a {@link FallbackIndicationListener}.
165     *
166     * @param listener listener to be unregistered.
167     */
168    public synchronized void removeFallbackIndicationListener(FallbackIndicationListener listener) {
169        listeners.remove(listener);
170    }
171}