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