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 (at) example.com' from='mary (at) 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 }