001/**
002 *
003 * Copyright 2003-2007 Jive Software.
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 */
017
018package org.jivesoftware.smackx.iqprivate;
019
020import org.jivesoftware.smack.Manager;
021import org.jivesoftware.smack.SmackException.NoResponseException;
022import org.jivesoftware.smack.SmackException.NotConnectedException;
023import org.jivesoftware.smack.XMPPConnection;
024import org.jivesoftware.smack.XMPPException.XMPPErrorException;
025import org.jivesoftware.smack.packet.IQ;
026import org.jivesoftware.smack.provider.IQProvider;
027import org.jivesoftware.smackx.iqprivate.packet.DefaultPrivateData;
028import org.jivesoftware.smackx.iqprivate.packet.PrivateData;
029import org.jivesoftware.smackx.iqprivate.provider.PrivateDataProvider;
030import org.xmlpull.v1.XmlPullParser;
031
032import java.util.Hashtable;
033import java.util.Map;
034import java.util.WeakHashMap;
035
036/**
037 * Manages private data, which is a mechanism to allow users to store arbitrary XML
038 * data on an XMPP server. Each private data chunk is defined by a element name and
039 * XML namespace. Example private data:
040 *
041 * <pre>
042 * &lt;color xmlns="http://example.com/xmpp/color"&gt;
043 *     &lt;favorite&gt;blue&lt;/blue&gt;
044 *     &lt;leastFavorite&gt;puce&lt;/leastFavorite&gt;
045 * &lt;/color&gt;
046 * </pre>
047 *
048 * {@link PrivateDataProvider} instances are responsible for translating the XML into objects.
049 * If no PrivateDataProvider is registered for a given element name and namespace, then
050 * a {@link DefaultPrivateData} instance will be returned.<p>
051 *
052 * Warning: this is an non-standard protocol documented by
053 * <a href="http://www.xmpp.org/extensions/jep-0049.html">XEP-49</a>. Because this is a
054 * non-standard protocol, it is subject to change.
055 *
056 * @author Matt Tucker
057 */
058public class PrivateDataManager extends Manager {
059    private static final Map<XMPPConnection, PrivateDataManager> instances = new WeakHashMap<XMPPConnection, PrivateDataManager>();
060
061    public static synchronized PrivateDataManager getInstanceFor(XMPPConnection connection) {
062        PrivateDataManager privateDataManager = instances.get(connection);
063        if (privateDataManager == null) {
064            privateDataManager = new PrivateDataManager(connection);
065        }
066        return privateDataManager;
067    }
068
069    /**
070     * Map of provider instances.
071     */
072    private static Map<String, PrivateDataProvider> privateDataProviders = new Hashtable<String, PrivateDataProvider>();
073
074    /**
075     * Returns the private data provider registered to the specified XML element name and namespace.
076     * For example, if a provider was registered to the element name "prefs" and the
077     * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger
078     * the provider:
079     *
080     * <pre>
081     * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
082     *     &lt;query xmlns='jabber:iq:private'&gt;
083     *         &lt;prefs xmlns='http://www.xmppclient.com/prefs'&gt;
084     *             &lt;value1&gt;ABC&lt;/value1&gt;
085     *             &lt;value2&gt;XYZ&lt;/value2&gt;
086     *         &lt;/prefs&gt;
087     *     &lt;/query&gt;
088     * &lt;/iq&gt;</pre>
089     *
090     * <p>Note: this method is generally only called by the internal Smack classes.
091     *
092     * @param elementName the XML element name.
093     * @param namespace the XML namespace.
094     * @return the PrivateData provider.
095     */
096    public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) {
097        String key = getProviderKey(elementName, namespace);
098        return (PrivateDataProvider)privateDataProviders.get(key);
099    }
100
101    /**
102     * Adds a private data provider with the specified element name and name space. The provider
103     * will override any providers loaded through the classpath.
104     *
105     * @param elementName the XML element name.
106     * @param namespace the XML namespace.
107     * @param provider the private data provider.
108     */
109    public static void addPrivateDataProvider(String elementName, String namespace,
110            PrivateDataProvider provider)
111    {
112        String key = getProviderKey(elementName, namespace);
113        privateDataProviders.put(key, provider);
114    }
115
116    /**
117     * Removes a private data provider with the specified element name and namespace.
118     *
119     * @param elementName The XML element name.
120     * @param namespace The XML namespace.
121     */
122    public static void removePrivateDataProvider(String elementName, String namespace) {
123        String key = getProviderKey(elementName, namespace);
124        privateDataProviders.remove(key);
125    }
126
127    /**
128     * Creates a new private data manager.
129     *
130     * @param connection an XMPP connection which must have already undergone a
131     *      successful login.
132     */
133    private PrivateDataManager(XMPPConnection connection) {
134        super(connection);
135        instances.put(connection, this);
136    }
137
138    /**
139     * Returns the private data specified by the given element name and namespace. Each chunk
140     * of private data is uniquely identified by an element name and namespace pair.<p>
141     *
142     * If a PrivateDataProvider is registered for the specified element name/namespace pair then
143     * that provider will determine the specific object type that is returned. If no provider
144     * is registered, a {@link DefaultPrivateData} instance will be returned.
145     *
146     * @param elementName the element name.
147     * @param namespace the namespace.
148     * @return the private data.
149     * @throws XMPPErrorException 
150     * @throws NoResponseException 
151     * @throws NotConnectedException 
152     */
153    public PrivateData getPrivateData(final String elementName, final String namespace) throws NoResponseException, XMPPErrorException, NotConnectedException
154    {
155        // Create an IQ packet to get the private data.
156        IQ privateDataGet = new IQ() {
157            public String getChildElementXML() {
158                StringBuilder buf = new StringBuilder();
159                buf.append("<query xmlns=\"jabber:iq:private\">");
160                buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>");
161                buf.append("</query>");
162                return buf.toString();
163            }
164        };
165        privateDataGet.setType(IQ.Type.GET);
166
167        PrivateDataResult response = (PrivateDataResult) connection().createPacketCollectorAndSend(
168                        privateDataGet).nextResultOrThrow();
169        return response.getPrivateData();
170    }
171
172    /**
173     * Sets a private data value. Each chunk of private data is uniquely identified by an
174     * element name and namespace pair. If private data has already been set with the
175     * element name and namespace, then the new private data will overwrite the old value.
176     *
177     * @param privateData the private data.
178     * @throws XMPPErrorException 
179     * @throws NoResponseException 
180     * @throws NotConnectedException 
181     */
182    public void setPrivateData(final PrivateData privateData) throws NoResponseException, XMPPErrorException, NotConnectedException {
183        // Create an IQ packet to set the private data.
184        IQ privateDataSet = new IQ() {
185            public String getChildElementXML() {
186                StringBuilder buf = new StringBuilder();
187                buf.append("<query xmlns=\"jabber:iq:private\">");
188                buf.append(privateData.toXML());
189                buf.append("</query>");
190                return buf.toString();
191            }
192        };
193        privateDataSet.setType(IQ.Type.SET);
194
195        connection().createPacketCollectorAndSend(privateDataSet).nextResultOrThrow();
196    }
197
198    /**
199     * Returns a String key for a given element name and namespace.
200     *
201     * @param elementName the element name.
202     * @param namespace the namespace.
203     * @return a unique key for the element name and namespace pair.
204     */
205    private static String getProviderKey(String elementName, String namespace) {
206        StringBuilder buf = new StringBuilder();
207        buf.append("<").append(elementName).append("/><").append(namespace).append("/>");
208        return buf.toString();
209    }
210
211    /**
212     * An IQ provider to parse IQ results containing private data.
213     */
214    public static class PrivateDataIQProvider implements IQProvider {
215        public IQ parseIQ(XmlPullParser parser) throws Exception {
216            PrivateData privateData = null;
217            boolean done = false;
218            while (!done) {
219                int eventType = parser.next();
220                if (eventType == XmlPullParser.START_TAG) {
221                    String elementName = parser.getName();
222                    String namespace = parser.getNamespace();
223                    // See if any objects are registered to handle this private data type.
224                    PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace);
225                    // If there is a registered provider, use it.
226                    if (provider != null) {
227                        privateData = provider.parsePrivateData(parser);
228                    }
229                    // Otherwise, use a DefaultPrivateData instance to store the private data.
230                    else {
231                        DefaultPrivateData data = new DefaultPrivateData(elementName, namespace);
232                        boolean finished = false;
233                        while (!finished) {
234                            int event = parser.next();
235                            if (event == XmlPullParser.START_TAG) {
236                                String name = parser.getName();
237                                // If an empty element, set the value with the empty string.
238                                if (parser.isEmptyElementTag()) {
239                                    data.setValue(name,"");
240                                }
241                                // Otherwise, get the the element text.
242                                else {
243                                    event = parser.next();
244                                    if (event == XmlPullParser.TEXT) {
245                                        String value = parser.getText();
246                                        data.setValue(name, value);
247                                    }
248                                }
249                            }
250                            else if (event == XmlPullParser.END_TAG) {
251                                if (parser.getName().equals(elementName)) {
252                                    finished = true;
253                                }
254                            }
255                        }
256                        privateData = data;
257                    }
258                }
259                else if (eventType == XmlPullParser.END_TAG) {
260                    if (parser.getName().equals("query")) {
261                        done = true;
262                    }
263                }
264            }
265            return new PrivateDataResult(privateData);
266        }
267    }
268
269    /**
270     * An IQ packet to hold PrivateData GET results.
271     */
272    private static class PrivateDataResult extends IQ {
273
274        private PrivateData privateData;
275
276        PrivateDataResult(PrivateData privateData) {
277            this.privateData = privateData;
278        }
279
280        public PrivateData getPrivateData() {
281            return privateData;
282        }
283
284        public String getChildElementXML() {
285            StringBuilder buf = new StringBuilder();
286            buf.append("<query xmlns=\"jabber:iq:private\">");
287            if (privateData != null) {
288                buf.append(privateData.toXML());
289            }
290            buf.append("</query>");
291            return buf.toString();
292        }
293    }
294}