1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smackx;
22
23import org.jivesoftware.smack.PacketCollector;
24import org.jivesoftware.smack.SmackConfiguration;
25import org.jivesoftware.smack.Connection;
26import org.jivesoftware.smack.XMPPException;
27import org.jivesoftware.smack.filter.PacketIDFilter;
28import org.jivesoftware.smack.packet.IQ;
29import org.jivesoftware.smack.provider.IQProvider;
30import org.jivesoftware.smackx.packet.DefaultPrivateData;
31import org.jivesoftware.smackx.packet.PrivateData;
32import org.jivesoftware.smackx.provider.PrivateDataProvider;
33import org.xmlpull.v1.XmlPullParser;
34
35import java.util.Hashtable;
36import java.util.Map;
37
38/**
39 * Manages private data, which is a mechanism to allow users to store arbitrary XML
40 * data on an XMPP server. Each private data chunk is defined by a element name and
41 * XML namespace. Example private data:
42 *
43 * <pre>
44 * &lt;color xmlns="http://example.com/xmpp/color"&gt;
45 *     &lt;favorite&gt;blue&lt;/blue&gt;
46 *     &lt;leastFavorite&gt;puce&lt;/leastFavorite&gt;
47 * &lt;/color&gt;
48 * </pre>
49 *
50 * {@link PrivateDataProvider} instances are responsible for translating the XML into objects.
51 * If no PrivateDataProvider is registered for a given element name and namespace, then
52 * a {@link DefaultPrivateData} instance will be returned.<p>
53 *
54 * Warning: this is an non-standard protocol documented by
55 * <a href="http://www.jabber.org/jeps/jep-0049.html">JEP-49</a>. Because this is a
56 * non-standard protocol, it is subject to change.
57 *
58 * @author Matt Tucker
59 */
60public class PrivateDataManager {
61
62    /**
63     * Map of provider instances.
64     */
65    private static Map<String, PrivateDataProvider> privateDataProviders = new Hashtable<String, PrivateDataProvider>();
66
67    /**
68     * Returns the private data provider registered to the specified XML element name and namespace.
69     * For example, if a provider was registered to the element name "prefs" and the
70     * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger
71     * the provider:
72     *
73     * <pre>
74     * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
75     *     &lt;query xmlns='jabber:iq:private'&gt;
76     *         &lt;prefs xmlns='http://www.xmppclient.com/prefs'&gt;
77     *             &lt;value1&gt;ABC&lt;/value1&gt;
78     *             &lt;value2&gt;XYZ&lt;/value2&gt;
79     *         &lt;/prefs&gt;
80     *     &lt;/query&gt;
81     * &lt;/iq&gt;</pre>
82     *
83     * <p>Note: this method is generally only called by the internal Smack classes.
84     *
85     * @param elementName the XML element name.
86     * @param namespace the XML namespace.
87     * @return the PrivateData provider.
88     */
89    public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) {
90        String key = getProviderKey(elementName, namespace);
91        return (PrivateDataProvider)privateDataProviders.get(key);
92    }
93
94    /**
95     * Adds a private data provider with the specified element name and name space. The provider
96     * will override any providers loaded through the classpath.
97     *
98     * @param elementName the XML element name.
99     * @param namespace the XML namespace.
100     * @param provider the private data provider.
101     */
102    public static void addPrivateDataProvider(String elementName, String namespace,
103            PrivateDataProvider provider)
104    {
105        String key = getProviderKey(elementName, namespace);
106        privateDataProviders.put(key, provider);
107    }
108
109    /**
110     * Removes a private data provider with the specified element name and namespace.
111     *
112     * @param elementName The XML element name.
113     * @param namespace The XML namespace.
114     */
115    public static void removePrivateDataProvider(String elementName, String namespace) {
116        String key = getProviderKey(elementName, namespace);
117        privateDataProviders.remove(key);
118    }
119
120
121    private Connection connection;
122
123    /**
124     * The user to get and set private data for. In most cases, this value should
125     * be <tt>null</tt>, as the typical use of private data is to get and set
126     * your own private data and not others.
127     */
128    private String user;
129
130    /**
131     * Creates a new private data manager. The connection must have
132     * undergone a successful login before being used to construct an instance of
133     * this class.
134     *
135     * @param connection an XMPP connection which must have already undergone a
136     *      successful login.
137     */
138    public PrivateDataManager(Connection connection) {
139        if (!connection.isAuthenticated()) {
140            throw new IllegalStateException("Must be logged in to XMPP server.");
141        }
142        this.connection = connection;
143    }
144
145    /**
146     * Creates a new private data manager for a specific user (special case). Most
147     * servers only support getting and setting private data for the user that
148     * authenticated via the connection. However, some servers support the ability
149     * to get and set private data for other users (for example, if you are the
150     * administrator). The connection must have undergone a successful login before
151     * being used to construct an instance of this class.
152     *
153     * @param connection an XMPP connection which must have already undergone a
154     *      successful login.
155     * @param user the XMPP address of the user to get and set private data for.
156     */
157    public PrivateDataManager(Connection connection, String user) {
158        if (!connection.isAuthenticated()) {
159            throw new IllegalStateException("Must be logged in to XMPP server.");
160        }
161        this.connection = connection;
162        this.user = user;
163    }
164
165    /**
166     * Returns the private data specified by the given element name and namespace. Each chunk
167     * of private data is uniquely identified by an element name and namespace pair.<p>
168     *
169     * If a PrivateDataProvider is registered for the specified element name/namespace pair then
170     * that provider will determine the specific object type that is returned. If no provider
171     * is registered, a {@link DefaultPrivateData} instance will be returned.
172     *
173     * @param elementName the element name.
174     * @param namespace the namespace.
175     * @return the private data.
176     * @throws XMPPException if an error occurs getting the private data.
177     */
178    public PrivateData getPrivateData(final String elementName, final String namespace)
179            throws XMPPException
180    {
181        // Create an IQ packet to get the private data.
182        IQ privateDataGet = new IQ() {
183            public String getChildElementXML() {
184                StringBuilder buf = new StringBuilder();
185                buf.append("<query xmlns=\"jabber:iq:private\">");
186                buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>");
187                buf.append("</query>");
188                return buf.toString();
189            }
190        };
191        privateDataGet.setType(IQ.Type.GET);
192        // Address the packet to the other account if user has been set.
193        if (user != null) {
194            privateDataGet.setTo(user);
195        }
196
197        // Setup a listener for the reply to the set operation.
198        String packetID = privateDataGet.getPacketID();
199        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));
200
201        // Send the private data.
202        connection.sendPacket(privateDataGet);
203
204        // Wait up to five seconds for a response from the server.
205        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
206        // Stop queuing results
207        collector.cancel();
208        if (response == null) {
209            throw new XMPPException("No response from the server.");
210        }
211        // If the server replied with an error, throw an exception.
212        else if (response.getType() == IQ.Type.ERROR) {
213            throw new XMPPException(response.getError());
214        }
215        return ((PrivateDataResult)response).getPrivateData();
216    }
217
218    /**
219     * Sets a private data value. Each chunk of private data is uniquely identified by an
220     * element name and namespace pair. If private data has already been set with the
221     * element name and namespace, then the new private data will overwrite the old value.
222     *
223     * @param privateData the private data.
224     * @throws XMPPException if setting the private data fails.
225     */
226    public void setPrivateData(final PrivateData privateData) throws XMPPException {
227        // Create an IQ packet to set the private data.
228        IQ privateDataSet = new IQ() {
229            public String getChildElementXML() {
230                StringBuilder buf = new StringBuilder();
231                buf.append("<query xmlns=\"jabber:iq:private\">");
232                buf.append(privateData.toXML());
233                buf.append("</query>");
234                return buf.toString();
235            }
236        };
237        privateDataSet.setType(IQ.Type.SET);
238        // Address the packet to the other account if user has been set.
239        if (user != null) {
240            privateDataSet.setTo(user);
241        }
242
243        // Setup a listener for the reply to the set operation.
244        String packetID = privateDataSet.getPacketID();
245        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));
246
247        // Send the private data.
248        connection.sendPacket(privateDataSet);
249
250        // Wait up to five seconds for a response from the server.
251        IQ response = (IQ)collector.nextResult(5000);
252        // Stop queuing results
253        collector.cancel();
254        if (response == null) {
255            throw new XMPPException("No response from the server.");
256        }
257        // If the server replied with an error, throw an exception.
258        else if (response.getType() == IQ.Type.ERROR) {
259            throw new XMPPException(response.getError());
260        }
261    }
262
263    /**
264     * Returns a String key for a given element name and namespace.
265     *
266     * @param elementName the element name.
267     * @param namespace the namespace.
268     * @return a unique key for the element name and namespace pair.
269     */
270    private static String getProviderKey(String elementName, String namespace) {
271        StringBuilder buf = new StringBuilder();
272        buf.append("<").append(elementName).append("/><").append(namespace).append("/>");
273        return buf.toString();
274    }
275
276    /**
277     * An IQ provider to parse IQ results containing private data.
278     */
279    public static class PrivateDataIQProvider implements IQProvider {
280        public IQ parseIQ(XmlPullParser parser) throws Exception {
281            PrivateData privateData = null;
282            boolean done = false;
283            while (!done) {
284                int eventType = parser.next();
285                if (eventType == XmlPullParser.START_TAG) {
286                    String elementName = parser.getName();
287                    String namespace = parser.getNamespace();
288                    // See if any objects are registered to handle this private data type.
289                    PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace);
290                    // If there is a registered provider, use it.
291                    if (provider != null) {
292                        privateData = provider.parsePrivateData(parser);
293                    }
294                    // Otherwise, use a DefaultPrivateData instance to store the private data.
295                    else {
296                        DefaultPrivateData data = new DefaultPrivateData(elementName, namespace);
297                        boolean finished = false;
298                        while (!finished) {
299                            int event = parser.next();
300                            if (event == XmlPullParser.START_TAG) {
301                                String name = parser.getName();
302                                // If an empty element, set the value with the empty string.
303                                if (parser.isEmptyElementTag()) {
304                                    data.setValue(name,"");
305                                }
306                                // Otherwise, get the the element text.
307                                else {
308                                    event = parser.next();
309                                    if (event == XmlPullParser.TEXT) {
310                                        String value = parser.getText();
311                                        data.setValue(name, value);
312                                    }
313                                }
314                            }
315                            else if (event == XmlPullParser.END_TAG) {
316                                if (parser.getName().equals(elementName)) {
317                                    finished = true;
318                                }
319                            }
320                        }
321                        privateData = data;
322                    }
323                }
324                else if (eventType == XmlPullParser.END_TAG) {
325                    if (parser.getName().equals("query")) {
326                        done = true;
327                    }
328                }
329            }
330            return new PrivateDataResult(privateData);
331        }
332    }
333
334    /**
335     * An IQ packet to hold PrivateData GET results.
336     */
337    private static class PrivateDataResult extends IQ {
338
339        private PrivateData privateData;
340
341        PrivateDataResult(PrivateData privateData) {
342            this.privateData = privateData;
343        }
344
345        public PrivateData getPrivateData() {
346            return privateData;
347        }
348
349        public String getChildElementXML() {
350            StringBuilder buf = new StringBuilder();
351            buf.append("<query xmlns=\"jabber:iq:private\">");
352            if (privateData != null) {
353                privateData.toXML();
354            }
355            buf.append("</query>");
356            return buf.toString();
357        }
358    }
359}