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