001/**
002 *
003 * Copyright 2013 Georg Lukas
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.carbons;
018
019import java.util.Collections;
020import java.util.Map;
021import java.util.WeakHashMap;
022
023import org.jivesoftware.smack.SmackException;
024import org.jivesoftware.smack.SmackException.NoResponseException;
025import org.jivesoftware.smack.SmackException.NotConnectedException;
026import org.jivesoftware.smack.XMPPConnection;
027import org.jivesoftware.smack.ConnectionCreationListener;
028import org.jivesoftware.smack.Manager;
029import org.jivesoftware.smack.PacketListener;
030import org.jivesoftware.smack.XMPPException;
031import org.jivesoftware.smack.XMPPException.XMPPErrorException;
032import org.jivesoftware.smack.filter.IQReplyFilter;
033import org.jivesoftware.smack.packet.IQ;
034import org.jivesoftware.smack.packet.Message;
035import org.jivesoftware.smack.packet.Packet;
036import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
037import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
038
039/**
040 * Packet extension for XEP-0280: Message Carbons. This class implements
041 * the manager for registering {@link CarbonExtension} support, enabling and disabling
042 * message carbons.
043 *
044 * You should call enableCarbons() before sending your first undirected
045 * presence.
046 *
047 * @author Georg Lukas
048 */
049public class CarbonManager extends Manager {
050
051    private static Map<XMPPConnection, CarbonManager> instances =
052            Collections.synchronizedMap(new WeakHashMap<XMPPConnection, CarbonManager>());
053
054    static {
055        XMPPConnection.addConnectionCreationListener(new ConnectionCreationListener() {
056            public void connectionCreated(XMPPConnection connection) {
057                getInstanceFor(connection);
058            }
059        });
060    }
061    
062    private volatile boolean enabled_state = false;
063
064    private CarbonManager(XMPPConnection connection) {
065        super(connection);
066        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
067        sdm.addFeature(CarbonExtension.NAMESPACE);
068        instances.put(connection, this);
069    }
070
071    /**
072     * Obtain the CarbonManager responsible for a connection.
073     *
074     * @param connection the connection object.
075     *
076     * @return a CarbonManager instance
077     */
078    public static synchronized CarbonManager getInstanceFor(XMPPConnection connection) {
079        CarbonManager carbonManager = instances.get(connection);
080
081        if (carbonManager == null) {
082            carbonManager = new CarbonManager(connection);
083        }
084
085        return carbonManager;
086    }
087
088    private IQ carbonsEnabledIQ(final boolean new_state) {
089        IQ setIQ = new IQ() {
090            public String getChildElementXML() {
091                return "<" + (new_state? "enable" : "disable") + " xmlns='" + CarbonExtension.NAMESPACE + "'/>";
092            }
093        };
094        setIQ.setType(IQ.Type.SET);
095        return setIQ;
096    }
097
098    /**
099     * Returns true if XMPP Carbons are supported by the server.
100     * 
101     * @return true if supported
102     * @throws SmackException if there was no response from the server.
103     * @throws XMPPException 
104     */
105    public boolean isSupportedByServer() throws XMPPException, SmackException {
106        return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(
107                        connection().getServiceName(), CarbonExtension.NAMESPACE);
108    }
109
110    /**
111     * Notify server to change the carbons state. This method returns
112     * immediately and changes the variable when the reply arrives.
113     *
114     * You should first check for support using isSupportedByServer().
115     *
116     * @param new_state whether carbons should be enabled or disabled
117     * @throws NotConnectedException 
118     */
119    public void sendCarbonsEnabled(final boolean new_state) throws NotConnectedException {
120        IQ setIQ = carbonsEnabledIQ(new_state);
121
122        connection().addPacketListener(new PacketListener() {
123            public void processPacket(Packet packet) {
124                IQ result = (IQ)packet;
125                if (result.getType() == IQ.Type.RESULT) {
126                    enabled_state = new_state;
127                }
128                connection().removePacketListener(this);
129            }
130        }, new IQReplyFilter(setIQ, connection()));
131
132        connection().sendPacket(setIQ);
133    }
134
135    /**
136     * Notify server to change the carbons state. This method blocks
137     * some time until the server replies to the IQ and returns true on
138     * success.
139     *
140     * You should first check for support using isSupportedByServer().
141     *
142     * @param new_state whether carbons should be enabled or disabled
143     * @throws XMPPErrorException 
144     * @throws NoResponseException 
145     * @throws NotConnectedException 
146     *
147     */
148    public synchronized void setCarbonsEnabled(final boolean new_state) throws NoResponseException,
149                    XMPPErrorException, NotConnectedException {
150        if (enabled_state == new_state)
151            return;
152
153        IQ setIQ = carbonsEnabledIQ(new_state);
154
155        connection().createPacketCollectorAndSend(setIQ).nextResultOrThrow();
156        enabled_state = new_state;
157    }
158
159    /**
160     * Helper method to enable carbons.
161     *
162     * @throws XMPPException 
163     * @throws SmackException if there was no response from the server.
164     */
165    public void enableCarbons() throws XMPPException, SmackException {
166        setCarbonsEnabled(true);
167    }
168
169    /**
170     * Helper method to disable carbons.
171     *
172     * @throws XMPPException 
173     * @throws SmackException if there was no response from the server.
174     */
175    public void disableCarbons() throws XMPPException, SmackException {
176        setCarbonsEnabled(false);
177    }
178
179    /**
180     * Check if carbons are enabled on this connection.
181     */
182    public boolean getCarbonsEnabled() {
183        return this.enabled_state;
184    }
185
186    /**
187     * Obtain a Carbon from a message, if available.
188     *
189     * @param msg Message object to check for carbons
190     *
191     * @return a Carbon if available, null otherwise.
192     */
193    public static CarbonExtension getCarbon(Message msg) {
194        CarbonExtension cc = (CarbonExtension)msg.getExtension("received", CarbonExtension.NAMESPACE);
195        if (cc == null)
196            cc = (CarbonExtension)msg.getExtension("sent", CarbonExtension.NAMESPACE);
197        return cc;
198    }
199
200    /**
201     * Mark a message as "private", so it will not be carbon-copied.
202     *
203     * @param msg Message object to mark private
204     */
205    public static void disableCarbons(Message msg) {
206        msg.addExtension(new CarbonExtension.Private());
207    }
208}