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