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 * <color xmlns="http://example.com/xmpp/color"> 052 * <favorite>blue</blue> 053 * <leastFavorite>puce</leastFavorite> 054 * </color> 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 * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> 091 * <query xmlns='jabber:iq:private'> 092 * <prefs xmlns='http://www.xmppclient.com/prefs'> 093 * <value1>ABC</value1> 094 * <value2>XYZ</value2> 095 * </prefs> 096 * </query> 097 * </iq></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}