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