001/**
002 *
003 * Copyright 2016-2020 Fernando Ramirez, Florian Schmaus
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.bob;
018
019import java.util.Collections;
020import java.util.Map;
021import java.util.Set;
022import java.util.WeakHashMap;
023import java.util.concurrent.ConcurrentHashMap;
024
025import org.jivesoftware.smack.ConnectionCreationListener;
026import org.jivesoftware.smack.Manager;
027import org.jivesoftware.smack.SmackException.NoResponseException;
028import org.jivesoftware.smack.SmackException.NotConnectedException;
029import org.jivesoftware.smack.SmackException.NotLoggedInException;
030import org.jivesoftware.smack.XMPPConnection;
031import org.jivesoftware.smack.XMPPConnectionRegistry;
032import org.jivesoftware.smack.XMPPException.XMPPErrorException;
033import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
034import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
035import org.jivesoftware.smack.packet.IQ;
036import org.jivesoftware.smack.packet.IQ.Type;
037import org.jivesoftware.smack.util.SHA1;
038
039import org.jivesoftware.smackx.bob.element.BoBIQ;
040import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
041
042import org.jxmpp.jid.Jid;
043import org.jxmpp.util.cache.LruCache;
044
045/**
046 * Bits of Binary manager class.
047 *
048 * @author Fernando Ramirez
049 * @author Florian Schmaus
050 * @see <a href="http://xmpp.org/extensions/xep-0231.html">XEP-0231: Bits of
051 *      Binary</a>
052 */
053public final class BoBManager extends Manager {
054
055    public static final String NAMESPACE = "urn:xmpp:bob";
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 static final Map<XMPPConnection, BoBManager> INSTANCES = new WeakHashMap<>();
067
068    /**
069     * Get the singleton instance of BoBManager.
070     *
071     * @param connection TODO javadoc me please
072     * @return the instance of BoBManager
073     */
074    public static synchronized BoBManager getInstanceFor(XMPPConnection connection) {
075        BoBManager bobManager = INSTANCES.get(connection);
076        if (bobManager == null) {
077            bobManager = new BoBManager(connection);
078            INSTANCES.put(connection, bobManager);
079        }
080
081        return bobManager;
082    }
083
084    private static final LruCache<ContentId, BoBData> BOB_CACHE = new LruCache<>(128);
085
086    private final Map<ContentId, BoBInfo> bobs = new ConcurrentHashMap<>();
087
088    private BoBManager(XMPPConnection connection) {
089        super(connection);
090        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
091        serviceDiscoveryManager.addFeature(NAMESPACE);
092
093        connection.registerIQRequestHandler(
094                new AbstractIqRequestHandler(BoBIQ.ELEMENT, BoBIQ.NAMESPACE, Type.get, Mode.async) {
095                    @Override
096                    public IQ handleIQRequest(IQ iqRequest) {
097                        BoBIQ bobIQRequest = (BoBIQ) iqRequest;
098                        ContentId contentId = bobIQRequest.getContentId();
099
100                        BoBInfo bobInfo = bobs.get(contentId);
101                        if (bobInfo == null) {
102                            // TODO return item-not-found
103                            return null;
104                        }
105
106                        BoBData bobData = bobInfo.getData();
107                        BoBIQ responseBoBIQ = new BoBIQ(contentId, bobData);
108                        responseBoBIQ.setType(Type.result);
109                        responseBoBIQ.setTo(bobIQRequest.getFrom());
110                        return responseBoBIQ;
111                    }
112                });
113    }
114
115    /**
116     * Returns true if Bits of Binary is supported by the server.
117     *
118     * @return true if Bits of Binary is supported by the server.
119     * @throws NoResponseException if there was no response from the remote entity.
120     * @throws XMPPErrorException if there was an XMPP error returned.
121     * @throws NotConnectedException if the XMPP connection is not connected.
122     * @throws InterruptedException if the calling thread was interrupted.
123     */
124    public boolean isSupportedByServer()
125            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
126        return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(NAMESPACE);
127    }
128
129    /**
130     * Request BoB data.
131     *
132     * @param to TODO javadoc me please
133     * @param bobHash TODO javadoc me please
134     * @return the BoB data
135     * @throws NotLoggedInException if the XMPP connection is not authenticated.
136     * @throws NoResponseException if there was no response from the remote entity.
137     * @throws XMPPErrorException if there was an XMPP error returned.
138     * @throws NotConnectedException if the XMPP connection is not connected.
139     * @throws InterruptedException if the calling thread was interrupted.
140     */
141    public BoBData requestBoB(Jid to, ContentId bobHash) throws NotLoggedInException, NoResponseException,
142            XMPPErrorException, NotConnectedException, InterruptedException {
143        BoBData bobData = BOB_CACHE.lookup(bobHash);
144        if (bobData != null) {
145            return bobData;
146        }
147
148        BoBIQ requestBoBIQ = new BoBIQ(bobHash);
149        requestBoBIQ.setType(Type.get);
150        requestBoBIQ.setTo(to);
151
152        XMPPConnection connection = getAuthenticatedConnectionOrThrow();
153        BoBIQ responseBoBIQ = connection.createStanzaCollectorAndSend(requestBoBIQ).nextResultOrThrow();
154
155        bobData = responseBoBIQ.getBoBData();
156        BOB_CACHE.put(bobHash, bobData);
157
158        return bobData;
159    }
160
161    public BoBInfo addBoB(BoBData bobData) {
162        // We only support SHA-1 for now.
163        ContentId bobHash = new ContentId(SHA1.hex(bobData.getContent()), "sha1");
164
165        Set<ContentId> bobHashes = Collections.singleton(bobHash);
166        bobHashes = Collections.unmodifiableSet(bobHashes);
167
168        BoBInfo bobInfo = new BoBInfo(bobHashes, bobData);
169
170        bobs.put(bobHash, bobInfo);
171
172        return bobInfo;
173    }
174
175    public BoBInfo removeBoB(ContentId bobHash) {
176        BoBInfo bobInfo = bobs.remove(bobHash);
177        if (bobInfo == null) {
178            return null;
179        }
180        for (ContentId otherBobHash : bobInfo.getHashes()) {
181            bobs.remove(otherBobHash);
182        }
183        return bobInfo;
184    }
185}