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