aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/PrivateDataManager.java
blob: c6440bcf38b5a1fe3cf78d59097fa526834e7c2b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smackx;

import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smackx.packet.DefaultPrivateData;
import org.jivesoftware.smackx.packet.PrivateData;
import org.jivesoftware.smackx.provider.PrivateDataProvider;
import org.xmlpull.v1.XmlPullParser;

import java.util.Hashtable;
import java.util.Map;

/**
 * Manages private data, which is a mechanism to allow users to store arbitrary XML
 * data on an XMPP server. Each private data chunk is defined by a element name and
 * XML namespace. Example private data:
 *
 * <pre>
 * &lt;color xmlns="http://example.com/xmpp/color"&gt;
 *     &lt;favorite&gt;blue&lt;/blue&gt;
 *     &lt;leastFavorite&gt;puce&lt;/leastFavorite&gt;
 * &lt;/color&gt;
 * </pre>
 *
 * {@link PrivateDataProvider} instances are responsible for translating the XML into objects.
 * If no PrivateDataProvider is registered for a given element name and namespace, then
 * a {@link DefaultPrivateData} instance will be returned.<p>
 *
 * Warning: this is an non-standard protocol documented by
 * <a href="http://www.jabber.org/jeps/jep-0049.html">JEP-49</a>. Because this is a
 * non-standard protocol, it is subject to change.
 *
 * @author Matt Tucker
 */
public class PrivateDataManager {

    /**
     * Map of provider instances.
     */
    private static Map<String, PrivateDataProvider> privateDataProviders = new Hashtable<String, PrivateDataProvider>();

    /**
     * Returns the private data provider registered to the specified XML element name and namespace.
     * For example, if a provider was registered to the element name "prefs" and the
     * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger
     * the provider:
     *
     * <pre>
     * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
     *     &lt;query xmlns='jabber:iq:private'&gt;
     *         &lt;prefs xmlns='http://www.xmppclient.com/prefs'&gt;
     *             &lt;value1&gt;ABC&lt;/value1&gt;
     *             &lt;value2&gt;XYZ&lt;/value2&gt;
     *         &lt;/prefs&gt;
     *     &lt;/query&gt;
     * &lt;/iq&gt;</pre>
     *
     * <p>Note: this method is generally only called by the internal Smack classes.
     *
     * @param elementName the XML element name.
     * @param namespace the XML namespace.
     * @return the PrivateData provider.
     */
    public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) {
        String key = getProviderKey(elementName, namespace);
        return (PrivateDataProvider)privateDataProviders.get(key);
    }

    /**
     * Adds a private data provider with the specified element name and name space. The provider
     * will override any providers loaded through the classpath.
     *
     * @param elementName the XML element name.
     * @param namespace the XML namespace.
     * @param provider the private data provider.
     */
    public static void addPrivateDataProvider(String elementName, String namespace,
            PrivateDataProvider provider)
    {
        String key = getProviderKey(elementName, namespace);
        privateDataProviders.put(key, provider);
    }

    /**
     * Removes a private data provider with the specified element name and namespace.
     *
     * @param elementName The XML element name.
     * @param namespace The XML namespace.
     */
    public static void removePrivateDataProvider(String elementName, String namespace) {
        String key = getProviderKey(elementName, namespace);
        privateDataProviders.remove(key);
    }


    private Connection connection;

    /**
     * The user to get and set private data for. In most cases, this value should
     * be <tt>null</tt>, as the typical use of private data is to get and set
     * your own private data and not others.
     */
    private String user;

    /**
     * Creates a new private data manager. The connection must have
     * undergone a successful login before being used to construct an instance of
     * this class.
     *
     * @param connection an XMPP connection which must have already undergone a
     *      successful login.
     */
    public PrivateDataManager(Connection connection) {
        if (!connection.isAuthenticated()) {
            throw new IllegalStateException("Must be logged in to XMPP server.");
        }
        this.connection = connection;
    }

    /**
     * Creates a new private data manager for a specific user (special case). Most
     * servers only support getting and setting private data for the user that
     * authenticated via the connection. However, some servers support the ability
     * to get and set private data for other users (for example, if you are the
     * administrator). The connection must have undergone a successful login before
     * being used to construct an instance of this class.
     *
     * @param connection an XMPP connection which must have already undergone a
     *      successful login.
     * @param user the XMPP address of the user to get and set private data for.
     */
    public PrivateDataManager(Connection connection, String user) {
        if (!connection.isAuthenticated()) {
            throw new IllegalStateException("Must be logged in to XMPP server.");
        }
        this.connection = connection;
        this.user = user;
    }

    /**
     * Returns the private data specified by the given element name and namespace. Each chunk
     * of private data is uniquely identified by an element name and namespace pair.<p>
     *
     * If a PrivateDataProvider is registered for the specified element name/namespace pair then
     * that provider will determine the specific object type that is returned. If no provider
     * is registered, a {@link DefaultPrivateData} instance will be returned.
     *
     * @param elementName the element name.
     * @param namespace the namespace.
     * @return the private data.
     * @throws XMPPException if an error occurs getting the private data.
     */
    public PrivateData getPrivateData(final String elementName, final String namespace)
            throws XMPPException
    {
        // Create an IQ packet to get the private data.
        IQ privateDataGet = new IQ() {
            public String getChildElementXML() {
                StringBuilder buf = new StringBuilder();
                buf.append("<query xmlns=\"jabber:iq:private\">");
                buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>");
                buf.append("</query>");
                return buf.toString();
            }
        };
        privateDataGet.setType(IQ.Type.GET);
        // Address the packet to the other account if user has been set.
        if (user != null) {
            privateDataGet.setTo(user);
        }

        // Setup a listener for the reply to the set operation.
        String packetID = privateDataGet.getPacketID();
        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));

        // Send the private data.
        connection.sendPacket(privateDataGet);

        // Wait up to five seconds for a response from the server.
        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from the server.");
        }
        // If the server replied with an error, throw an exception.
        else if (response.getType() == IQ.Type.ERROR) {
            throw new XMPPException(response.getError());
        }
        return ((PrivateDataResult)response).getPrivateData();
    }

    /**
     * Sets a private data value. Each chunk of private data is uniquely identified by an
     * element name and namespace pair. If private data has already been set with the
     * element name and namespace, then the new private data will overwrite the old value.
     *
     * @param privateData the private data.
     * @throws XMPPException if setting the private data fails.
     */
    public void setPrivateData(final PrivateData privateData) throws XMPPException {
        // Create an IQ packet to set the private data.
        IQ privateDataSet = new IQ() {
            public String getChildElementXML() {
                StringBuilder buf = new StringBuilder();
                buf.append("<query xmlns=\"jabber:iq:private\">");
                buf.append(privateData.toXML());
                buf.append("</query>");
                return buf.toString();
            }
        };
        privateDataSet.setType(IQ.Type.SET);
        // Address the packet to the other account if user has been set.
        if (user != null) {
            privateDataSet.setTo(user);
        }

        // Setup a listener for the reply to the set operation.
        String packetID = privateDataSet.getPacketID();
        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));

        // Send the private data.
        connection.sendPacket(privateDataSet);

        // Wait up to five seconds for a response from the server.
        IQ response = (IQ)collector.nextResult(5000);
        // Stop queuing results
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from the server.");
        }
        // If the server replied with an error, throw an exception.
        else if (response.getType() == IQ.Type.ERROR) {
            throw new XMPPException(response.getError());
        }
    }

    /**
     * Returns a String key for a given element name and namespace.
     *
     * @param elementName the element name.
     * @param namespace the namespace.
     * @return a unique key for the element name and namespace pair.
     */
    private static String getProviderKey(String elementName, String namespace) {
        StringBuilder buf = new StringBuilder();
        buf.append("<").append(elementName).append("/><").append(namespace).append("/>");
        return buf.toString();
    }

    /**
     * An IQ provider to parse IQ results containing private data.
     */
    public static class PrivateDataIQProvider implements IQProvider {
        public IQ parseIQ(XmlPullParser parser) throws Exception {
            PrivateData privateData = null;
            boolean done = false;
            while (!done) {
                int eventType = parser.next();
                if (eventType == XmlPullParser.START_TAG) {
                    String elementName = parser.getName();
                    String namespace = parser.getNamespace();
                    // See if any objects are registered to handle this private data type.
                    PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace);
                    // If there is a registered provider, use it.
                    if (provider != null) {
                        privateData = provider.parsePrivateData(parser);
                    }
                    // Otherwise, use a DefaultPrivateData instance to store the private data.
                    else {
                        DefaultPrivateData data = new DefaultPrivateData(elementName, namespace);
                        boolean finished = false;
                        while (!finished) {
                            int event = parser.next();
                            if (event == XmlPullParser.START_TAG) {
                                String name = parser.getName();
                                // If an empty element, set the value with the empty string.
                                if (parser.isEmptyElementTag()) {
                                    data.setValue(name,"");
                                }
                                // Otherwise, get the the element text.
                                else {
                                    event = parser.next();
                                    if (event == XmlPullParser.TEXT) {
                                        String value = parser.getText();
                                        data.setValue(name, value);
                                    }
                                }
                            }
                            else if (event == XmlPullParser.END_TAG) {
                                if (parser.getName().equals(elementName)) {
                                    finished = true;
                                }
                            }
                        }
                        privateData = data;
                    }
                }
                else if (eventType == XmlPullParser.END_TAG) {
                    if (parser.getName().equals("query")) {
                        done = true;
                    }
                }
            }
            return new PrivateDataResult(privateData);
        }
    }

    /**
     * An IQ packet to hold PrivateData GET results.
     */
    private static class PrivateDataResult extends IQ {

        private PrivateData privateData;

        PrivateDataResult(PrivateData privateData) {
            this.privateData = privateData;
        }

        public PrivateData getPrivateData() {
            return privateData;
        }

        public String getChildElementXML() {
            StringBuilder buf = new StringBuilder();
            buf.append("<query xmlns=\"jabber:iq:private\">");
            if (privateData != null) {
                privateData.toXML();
            }
            buf.append("</query>");
            return buf.toString();
        }
    }
}