Shuyi Chen | d7955ce | 2013-05-22 14:51:55 -0700 | [diff] [blame] | 1 | /** |
| 2 | * $RCSfile$ |
| 3 | * $Revision$ |
| 4 | * $Date$ |
| 5 | * |
| 6 | * Copyright 2003-2007 Jive Software. |
| 7 | * |
| 8 | * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); |
| 9 | * you may not use this file except in compliance with the License. |
| 10 | * You may obtain a copy of the License at |
| 11 | * |
| 12 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 13 | * |
| 14 | * Unless required by applicable law or agreed to in writing, software |
| 15 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 17 | * See the License for the specific language governing permissions and |
| 18 | * limitations under the License. |
| 19 | */ |
| 20 | |
| 21 | package org.jivesoftware.smackx; |
| 22 | |
| 23 | import org.jivesoftware.smack.PacketCollector; |
| 24 | import org.jivesoftware.smack.SmackConfiguration; |
| 25 | import org.jivesoftware.smack.Connection; |
| 26 | import org.jivesoftware.smack.XMPPException; |
| 27 | import org.jivesoftware.smack.filter.PacketIDFilter; |
| 28 | import org.jivesoftware.smack.packet.IQ; |
| 29 | import org.jivesoftware.smack.provider.IQProvider; |
| 30 | import org.jivesoftware.smackx.packet.DefaultPrivateData; |
| 31 | import org.jivesoftware.smackx.packet.PrivateData; |
| 32 | import org.jivesoftware.smackx.provider.PrivateDataProvider; |
| 33 | import org.xmlpull.v1.XmlPullParser; |
| 34 | |
| 35 | import java.util.Hashtable; |
| 36 | import java.util.Map; |
| 37 | |
| 38 | /** |
| 39 | * Manages private data, which is a mechanism to allow users to store arbitrary XML |
| 40 | * data on an XMPP server. Each private data chunk is defined by a element name and |
| 41 | * XML namespace. Example private data: |
| 42 | * |
| 43 | * <pre> |
| 44 | * <color xmlns="http://example.com/xmpp/color"> |
| 45 | * <favorite>blue</blue> |
| 46 | * <leastFavorite>puce</leastFavorite> |
| 47 | * </color> |
| 48 | * </pre> |
| 49 | * |
| 50 | * {@link PrivateDataProvider} instances are responsible for translating the XML into objects. |
| 51 | * If no PrivateDataProvider is registered for a given element name and namespace, then |
| 52 | * a {@link DefaultPrivateData} instance will be returned.<p> |
| 53 | * |
| 54 | * Warning: this is an non-standard protocol documented by |
| 55 | * <a href="http://www.jabber.org/jeps/jep-0049.html">JEP-49</a>. Because this is a |
| 56 | * non-standard protocol, it is subject to change. |
| 57 | * |
| 58 | * @author Matt Tucker |
| 59 | */ |
| 60 | public class PrivateDataManager { |
| 61 | |
| 62 | /** |
| 63 | * Map of provider instances. |
| 64 | */ |
| 65 | private static Map<String, PrivateDataProvider> privateDataProviders = new Hashtable<String, PrivateDataProvider>(); |
| 66 | |
| 67 | /** |
| 68 | * Returns the private data provider registered to the specified XML element name and namespace. |
| 69 | * For example, if a provider was registered to the element name "prefs" and the |
| 70 | * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger |
| 71 | * the provider: |
| 72 | * |
| 73 | * <pre> |
| 74 | * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> |
| 75 | * <query xmlns='jabber:iq:private'> |
| 76 | * <prefs xmlns='http://www.xmppclient.com/prefs'> |
| 77 | * <value1>ABC</value1> |
| 78 | * <value2>XYZ</value2> |
| 79 | * </prefs> |
| 80 | * </query> |
| 81 | * </iq></pre> |
| 82 | * |
| 83 | * <p>Note: this method is generally only called by the internal Smack classes. |
| 84 | * |
| 85 | * @param elementName the XML element name. |
| 86 | * @param namespace the XML namespace. |
| 87 | * @return the PrivateData provider. |
| 88 | */ |
| 89 | public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) { |
| 90 | String key = getProviderKey(elementName, namespace); |
| 91 | return (PrivateDataProvider)privateDataProviders.get(key); |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Adds a private data provider with the specified element name and name space. The provider |
| 96 | * will override any providers loaded through the classpath. |
| 97 | * |
| 98 | * @param elementName the XML element name. |
| 99 | * @param namespace the XML namespace. |
| 100 | * @param provider the private data provider. |
| 101 | */ |
| 102 | public static void addPrivateDataProvider(String elementName, String namespace, |
| 103 | PrivateDataProvider provider) |
| 104 | { |
| 105 | String key = getProviderKey(elementName, namespace); |
| 106 | privateDataProviders.put(key, provider); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Removes a private data provider with the specified element name and namespace. |
| 111 | * |
| 112 | * @param elementName The XML element name. |
| 113 | * @param namespace The XML namespace. |
| 114 | */ |
| 115 | public static void removePrivateDataProvider(String elementName, String namespace) { |
| 116 | String key = getProviderKey(elementName, namespace); |
| 117 | privateDataProviders.remove(key); |
| 118 | } |
| 119 | |
| 120 | |
| 121 | private Connection connection; |
| 122 | |
| 123 | /** |
| 124 | * The user to get and set private data for. In most cases, this value should |
| 125 | * be <tt>null</tt>, as the typical use of private data is to get and set |
| 126 | * your own private data and not others. |
| 127 | */ |
| 128 | private String user; |
| 129 | |
| 130 | /** |
| 131 | * Creates a new private data manager. The connection must have |
| 132 | * undergone a successful login before being used to construct an instance of |
| 133 | * this class. |
| 134 | * |
| 135 | * @param connection an XMPP connection which must have already undergone a |
| 136 | * successful login. |
| 137 | */ |
| 138 | public PrivateDataManager(Connection connection) { |
| 139 | if (!connection.isAuthenticated()) { |
| 140 | throw new IllegalStateException("Must be logged in to XMPP server."); |
| 141 | } |
| 142 | this.connection = connection; |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Creates a new private data manager for a specific user (special case). Most |
| 147 | * servers only support getting and setting private data for the user that |
| 148 | * authenticated via the connection. However, some servers support the ability |
| 149 | * to get and set private data for other users (for example, if you are the |
| 150 | * administrator). The connection must have undergone a successful login before |
| 151 | * being used to construct an instance of this class. |
| 152 | * |
| 153 | * @param connection an XMPP connection which must have already undergone a |
| 154 | * successful login. |
| 155 | * @param user the XMPP address of the user to get and set private data for. |
| 156 | */ |
| 157 | public PrivateDataManager(Connection connection, String user) { |
| 158 | if (!connection.isAuthenticated()) { |
| 159 | throw new IllegalStateException("Must be logged in to XMPP server."); |
| 160 | } |
| 161 | this.connection = connection; |
| 162 | this.user = user; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Returns the private data specified by the given element name and namespace. Each chunk |
| 167 | * of private data is uniquely identified by an element name and namespace pair.<p> |
| 168 | * |
| 169 | * If a PrivateDataProvider is registered for the specified element name/namespace pair then |
| 170 | * that provider will determine the specific object type that is returned. If no provider |
| 171 | * is registered, a {@link DefaultPrivateData} instance will be returned. |
| 172 | * |
| 173 | * @param elementName the element name. |
| 174 | * @param namespace the namespace. |
| 175 | * @return the private data. |
| 176 | * @throws XMPPException if an error occurs getting the private data. |
| 177 | */ |
| 178 | public PrivateData getPrivateData(final String elementName, final String namespace) |
| 179 | throws XMPPException |
| 180 | { |
| 181 | // Create an IQ packet to get the private data. |
| 182 | IQ privateDataGet = new IQ() { |
| 183 | public String getChildElementXML() { |
| 184 | StringBuilder buf = new StringBuilder(); |
| 185 | buf.append("<query xmlns=\"jabber:iq:private\">"); |
| 186 | buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>"); |
| 187 | buf.append("</query>"); |
| 188 | return buf.toString(); |
| 189 | } |
| 190 | }; |
| 191 | privateDataGet.setType(IQ.Type.GET); |
| 192 | // Address the packet to the other account if user has been set. |
| 193 | if (user != null) { |
| 194 | privateDataGet.setTo(user); |
| 195 | } |
| 196 | |
| 197 | // Setup a listener for the reply to the set operation. |
| 198 | String packetID = privateDataGet.getPacketID(); |
| 199 | PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID)); |
| 200 | |
| 201 | // Send the private data. |
| 202 | connection.sendPacket(privateDataGet); |
| 203 | |
| 204 | // Wait up to five seconds for a response from the server. |
| 205 | IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); |
| 206 | // Stop queuing results |
| 207 | collector.cancel(); |
| 208 | if (response == null) { |
| 209 | throw new XMPPException("No response from the server."); |
| 210 | } |
| 211 | // If the server replied with an error, throw an exception. |
| 212 | else if (response.getType() == IQ.Type.ERROR) { |
| 213 | throw new XMPPException(response.getError()); |
| 214 | } |
| 215 | return ((PrivateDataResult)response).getPrivateData(); |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Sets a private data value. Each chunk of private data is uniquely identified by an |
| 220 | * element name and namespace pair. If private data has already been set with the |
| 221 | * element name and namespace, then the new private data will overwrite the old value. |
| 222 | * |
| 223 | * @param privateData the private data. |
| 224 | * @throws XMPPException if setting the private data fails. |
| 225 | */ |
| 226 | public void setPrivateData(final PrivateData privateData) throws XMPPException { |
| 227 | // Create an IQ packet to set the private data. |
| 228 | IQ privateDataSet = new IQ() { |
| 229 | public String getChildElementXML() { |
| 230 | StringBuilder buf = new StringBuilder(); |
| 231 | buf.append("<query xmlns=\"jabber:iq:private\">"); |
| 232 | buf.append(privateData.toXML()); |
| 233 | buf.append("</query>"); |
| 234 | return buf.toString(); |
| 235 | } |
| 236 | }; |
| 237 | privateDataSet.setType(IQ.Type.SET); |
| 238 | // Address the packet to the other account if user has been set. |
| 239 | if (user != null) { |
| 240 | privateDataSet.setTo(user); |
| 241 | } |
| 242 | |
| 243 | // Setup a listener for the reply to the set operation. |
| 244 | String packetID = privateDataSet.getPacketID(); |
| 245 | PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID)); |
| 246 | |
| 247 | // Send the private data. |
| 248 | connection.sendPacket(privateDataSet); |
| 249 | |
| 250 | // Wait up to five seconds for a response from the server. |
| 251 | IQ response = (IQ)collector.nextResult(5000); |
| 252 | // Stop queuing results |
| 253 | collector.cancel(); |
| 254 | if (response == null) { |
| 255 | throw new XMPPException("No response from the server."); |
| 256 | } |
| 257 | // If the server replied with an error, throw an exception. |
| 258 | else if (response.getType() == IQ.Type.ERROR) { |
| 259 | throw new XMPPException(response.getError()); |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | /** |
| 264 | * Returns a String key for a given element name and namespace. |
| 265 | * |
| 266 | * @param elementName the element name. |
| 267 | * @param namespace the namespace. |
| 268 | * @return a unique key for the element name and namespace pair. |
| 269 | */ |
| 270 | private static String getProviderKey(String elementName, String namespace) { |
| 271 | StringBuilder buf = new StringBuilder(); |
| 272 | buf.append("<").append(elementName).append("/><").append(namespace).append("/>"); |
| 273 | return buf.toString(); |
| 274 | } |
| 275 | |
| 276 | /** |
| 277 | * An IQ provider to parse IQ results containing private data. |
| 278 | */ |
| 279 | public static class PrivateDataIQProvider implements IQProvider { |
| 280 | public IQ parseIQ(XmlPullParser parser) throws Exception { |
| 281 | PrivateData privateData = null; |
| 282 | boolean done = false; |
| 283 | while (!done) { |
| 284 | int eventType = parser.next(); |
| 285 | if (eventType == XmlPullParser.START_TAG) { |
| 286 | String elementName = parser.getName(); |
| 287 | String namespace = parser.getNamespace(); |
| 288 | // See if any objects are registered to handle this private data type. |
| 289 | PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace); |
| 290 | // If there is a registered provider, use it. |
| 291 | if (provider != null) { |
| 292 | privateData = provider.parsePrivateData(parser); |
| 293 | } |
| 294 | // Otherwise, use a DefaultPrivateData instance to store the private data. |
| 295 | else { |
| 296 | DefaultPrivateData data = new DefaultPrivateData(elementName, namespace); |
| 297 | boolean finished = false; |
| 298 | while (!finished) { |
| 299 | int event = parser.next(); |
| 300 | if (event == XmlPullParser.START_TAG) { |
| 301 | String name = parser.getName(); |
| 302 | // If an empty element, set the value with the empty string. |
| 303 | if (parser.isEmptyElementTag()) { |
| 304 | data.setValue(name,""); |
| 305 | } |
| 306 | // Otherwise, get the the element text. |
| 307 | else { |
| 308 | event = parser.next(); |
| 309 | if (event == XmlPullParser.TEXT) { |
| 310 | String value = parser.getText(); |
| 311 | data.setValue(name, value); |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | else if (event == XmlPullParser.END_TAG) { |
| 316 | if (parser.getName().equals(elementName)) { |
| 317 | finished = true; |
| 318 | } |
| 319 | } |
| 320 | } |
| 321 | privateData = data; |
| 322 | } |
| 323 | } |
| 324 | else if (eventType == XmlPullParser.END_TAG) { |
| 325 | if (parser.getName().equals("query")) { |
| 326 | done = true; |
| 327 | } |
| 328 | } |
| 329 | } |
| 330 | return new PrivateDataResult(privateData); |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | /** |
| 335 | * An IQ packet to hold PrivateData GET results. |
| 336 | */ |
| 337 | private static class PrivateDataResult extends IQ { |
| 338 | |
| 339 | private PrivateData privateData; |
| 340 | |
| 341 | PrivateDataResult(PrivateData privateData) { |
| 342 | this.privateData = privateData; |
| 343 | } |
| 344 | |
| 345 | public PrivateData getPrivateData() { |
| 346 | return privateData; |
| 347 | } |
| 348 | |
| 349 | public String getChildElementXML() { |
| 350 | StringBuilder buf = new StringBuilder(); |
| 351 | buf.append("<query xmlns=\"jabber:iq:private\">"); |
| 352 | if (privateData != null) { |
| 353 | privateData.toXML(); |
| 354 | } |
| 355 | buf.append("</query>"); |
| 356 | return buf.toString(); |
| 357 | } |
| 358 | } |
| 359 | } |