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