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