1/**
2 * Copyright 2013 Georg Lukas
3 *
4 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.jivesoftware.smackx.carbons;
18
19import java.util.Collections;
20import java.util.Map;
21import java.util.WeakHashMap;
22
23import org.jivesoftware.smack.Connection;
24import org.jivesoftware.smack.ConnectionCreationListener;
25import org.jivesoftware.smack.PacketCollector;
26import org.jivesoftware.smack.PacketListener;
27import org.jivesoftware.smack.SmackConfiguration;
28import org.jivesoftware.smack.XMPPException;
29import org.jivesoftware.smack.filter.PacketIDFilter;
30import org.jivesoftware.smack.packet.IQ;
31import org.jivesoftware.smack.packet.Message;
32import org.jivesoftware.smack.packet.Packet;
33import org.jivesoftware.smackx.ServiceDiscoveryManager;
34import org.jivesoftware.smackx.packet.DiscoverInfo;
35
36/**
37 * Packet extension for XEP-0280: Message Carbons. This class implements
38 * the manager for registering {@link Carbon} support, enabling and disabling
39 * message carbons.
40 *
41 * You should call enableCarbons() before sending your first undirected
42 * presence.
43 *
44 * @author Georg Lukas
45 */
46public class CarbonManager {
47
48    private static Map<Connection, CarbonManager> instances =
49            Collections.synchronizedMap(new WeakHashMap<Connection, CarbonManager>());
50
51    static {
52        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
53            public void connectionCreated(Connection connection) {
54                new CarbonManager(connection);
55            }
56        });
57    }
58
59    private Connection connection;
60    private volatile boolean enabled_state = false;
61
62    private CarbonManager(Connection connection) {
63        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
64        sdm.addFeature(Carbon.NAMESPACE);
65        this.connection = connection;
66        instances.put(connection, this);
67    }
68
69    /**
70     * Obtain the CarbonManager responsible for a connection.
71     *
72     * @param connection the connection object.
73     *
74     * @return a CarbonManager instance
75     */
76    public static CarbonManager getInstanceFor(Connection connection) {
77        CarbonManager carbonManager = instances.get(connection);
78
79        if (carbonManager == null) {
80            carbonManager = new CarbonManager(connection);
81        }
82
83        return carbonManager;
84    }
85
86    private IQ carbonsEnabledIQ(final boolean new_state) {
87        IQ setIQ = new IQ() {
88            public String getChildElementXML() {
89                return "<" + (new_state? "enable" : "disable") + " xmlns='" + Carbon.NAMESPACE + "'/>";
90            }
91        };
92        setIQ.setType(IQ.Type.SET);
93        return setIQ;
94    }
95
96    /**
97     * Returns true if XMPP Carbons are supported by the server.
98     *
99     * @return true if supported
100     */
101    public boolean isSupportedByServer() {
102        try {
103            DiscoverInfo result = ServiceDiscoveryManager
104                .getInstanceFor(connection).discoverInfo(connection.getServiceName());
105            return result.containsFeature(Carbon.NAMESPACE);
106        }
107        catch (XMPPException e) {
108            return false;
109        }
110    }
111
112    /**
113     * Notify server to change the carbons state. This method returns
114     * immediately and changes the variable when the reply arrives.
115     *
116     * You should first check for support using isSupportedByServer().
117     *
118     * @param new_state whether carbons should be enabled or disabled
119     */
120    public void sendCarbonsEnabled(final boolean new_state) {
121        IQ setIQ = carbonsEnabledIQ(new_state);
122
123        connection.addPacketListener(new PacketListener() {
124            public void processPacket(Packet packet) {
125                IQ result = (IQ)packet;
126                if (result.getType() == IQ.Type.RESULT) {
127                    enabled_state = new_state;
128                }
129                connection.removePacketListener(this);
130            }
131        }, new PacketIDFilter(setIQ.getPacketID()));
132
133        connection.sendPacket(setIQ);
134    }
135
136    /**
137     * Notify server to change the carbons state. This method blocks
138     * some time until the server replies to the IQ and returns true on
139     * success.
140     *
141     * You should first check for support using isSupportedByServer().
142     *
143     * @param new_state whether carbons should be enabled or disabled
144     *
145     * @return true if the operation was successful
146     */
147    public boolean setCarbonsEnabled(final boolean new_state) {
148        if (enabled_state == new_state)
149            return true;
150
151        IQ setIQ = carbonsEnabledIQ(new_state);
152
153        PacketCollector collector =
154                connection.createPacketCollector(new PacketIDFilter(setIQ.getPacketID()));
155        connection.sendPacket(setIQ);
156        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
157        collector.cancel();
158
159        if (result != null && result.getType() == IQ.Type.RESULT) {
160            enabled_state = new_state;
161            return true;
162        }
163        return false;
164    }
165
166    /**
167     * Helper method to enable carbons.
168     *
169     * @return true if the operation was successful
170     */
171    public boolean enableCarbons() {
172        return setCarbonsEnabled(true);
173    }
174
175    /**
176     * Helper method to disable carbons.
177     *
178     * @return true if the operation was successful
179     */
180    public boolean disableCarbons() {
181        return setCarbonsEnabled(false);
182    }
183
184    /**
185     * Check if carbons are enabled on this connection.
186     */
187    public boolean getCarbonsEnabled() {
188        return this.enabled_state;
189    }
190
191    /**
192     * Obtain a Carbon from a message, if available.
193     *
194     * @param msg Message object to check for carbons
195     *
196     * @return a Carbon if available, null otherwise.
197     */
198    public static Carbon getCarbon(Message msg) {
199        Carbon cc = (Carbon)msg.getExtension("received", Carbon.NAMESPACE);
200        if (cc == null)
201            cc = (Carbon)msg.getExtension("sent", Carbon.NAMESPACE);
202        return cc;
203    }
204
205    /**
206     * Mark a message as "private", so it will not be carbon-copied.
207     *
208     * @param msg Message object to mark private
209     */
210    public static void disableCarbons(Message msg) {
211        msg.addExtension(new Carbon.Private());
212    }
213}
214