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.smack.roster.packet; 019 020import org.jivesoftware.smack.packet.IQ; 021import org.jivesoftware.smack.packet.NamedElement; 022import org.jivesoftware.smack.packet.Stanza; 023import org.jivesoftware.smack.util.Objects; 024import org.jivesoftware.smack.util.StringUtils; 025import org.jivesoftware.smack.util.XmlStringBuilder; 026import org.jxmpp.jid.BareJid; 027 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.List; 031import java.util.Locale; 032import java.util.Set; 033import java.util.concurrent.CopyOnWriteArraySet; 034 035/** 036 * Represents XMPP roster packets. 037 * 038 * @author Matt Tucker 039 * @author Florian Schmaus 040 */ 041public class RosterPacket extends IQ { 042 043 public static final String ELEMENT = QUERY_ELEMENT; 044 public static final String NAMESPACE = "jabber:iq:roster"; 045 046 private final List<Item> rosterItems = new ArrayList<Item>(); 047 private String rosterVersion; 048 049 public RosterPacket() { 050 super(ELEMENT, NAMESPACE); 051 } 052 053 /** 054 * Adds a roster item to the packet. 055 * 056 * @param item a roster item. 057 */ 058 public void addRosterItem(Item item) { 059 synchronized (rosterItems) { 060 rosterItems.add(item); 061 } 062 } 063 064 /** 065 * Returns the number of roster items in this roster packet. 066 * 067 * @return the number of roster items. 068 */ 069 public int getRosterItemCount() { 070 synchronized (rosterItems) { 071 return rosterItems.size(); 072 } 073 } 074 075 /** 076 * Returns a copied list of the roster items in the packet. 077 * 078 * @return a copied list of the roster items in the packet. 079 */ 080 public List<Item> getRosterItems() { 081 synchronized (rosterItems) { 082 return new ArrayList<Item>(rosterItems); 083 } 084 } 085 086 @Override 087 protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder buf) { 088 buf.optAttribute("ver", rosterVersion); 089 buf.rightAngleBracket(); 090 091 synchronized (rosterItems) { 092 for (Item entry : rosterItems) { 093 buf.append(entry.toXML()); 094 } 095 } 096 return buf; 097 } 098 099 public String getVersion() { 100 return rosterVersion; 101 } 102 103 public void setVersion(String version) { 104 rosterVersion = version; 105 } 106 107 /** 108 * A roster item, which consists of a JID, their name, the type of subscription, and 109 * the groups the roster item belongs to. 110 */ 111 // TODO Make this class immutable. 112 public static class Item implements NamedElement { 113 114 /** 115 * The constant value "{@value}". 116 */ 117 public static final String ELEMENT = Stanza.ITEM; 118 119 public static final String GROUP = "group"; 120 121 private final BareJid jid; 122 123 /** 124 * TODO describe me. With link to the RFC. Is ask= attribute. 125 */ 126 private boolean subscriptionPending; 127 128 // TODO Make immutable. 129 private String name; 130 private ItemType itemType = ItemType.none; 131 private boolean approved; 132 private final Set<String> groupNames; 133 134 /** 135 * Creates a new roster item. 136 * 137 * @param jid 138 * @param name 139 */ 140 public Item(BareJid jid, String name) { 141 this(jid, name, false); 142 } 143 144 /** 145 * Creates a new roster item. 146 * 147 * @param jid the jid. 148 * @param name the user's name. 149 * @param subscriptionPending 150 */ 151 public Item(BareJid jid, String name, boolean subscriptionPending) { 152 this.jid = Objects.requireNonNull(jid); 153 this.name = name; 154 this.subscriptionPending = subscriptionPending; 155 groupNames = new CopyOnWriteArraySet<String>(); 156 } 157 158 @Override 159 public String getElementName() { 160 return ELEMENT; 161 } 162 163 /** 164 * Returns the user. 165 * 166 * @return the user. 167 * @deprecated use {@link #getJid()} instead. 168 */ 169 @Deprecated 170 public String getUser() { 171 return jid.toString(); 172 } 173 174 /** 175 * Returns the JID of this item. 176 * 177 * @return the JID. 178 */ 179 public BareJid getJid() { 180 return jid; 181 } 182 183 /** 184 * Returns the user's name. 185 * 186 * @return the user's name. 187 */ 188 public String getName() { 189 return name; 190 } 191 192 /** 193 * Sets the user's name. 194 * 195 * @param name the user's name. 196 */ 197 public void setName(String name) { 198 this.name = name; 199 } 200 201 /** 202 * Returns the roster item type. 203 * 204 * @return the roster item type. 205 */ 206 public ItemType getItemType() { 207 return itemType; 208 } 209 210 /** 211 * Sets the roster item type. 212 * 213 * @param itemType the roster item type. 214 */ 215 public void setItemType(ItemType itemType) { 216 this.itemType = Objects.requireNonNull(itemType, "itemType must not be null"); 217 } 218 219 public void setSubscriptionPending(boolean subscriptionPending) { 220 this.subscriptionPending = subscriptionPending; 221 } 222 223 public boolean isSubscriptionPending() { 224 return subscriptionPending; 225 } 226 227 /** 228 * Returns the roster item pre-approval state. 229 * 230 * @return the pre-approval state. 231 */ 232 public boolean isApproved() { 233 return approved; 234 } 235 236 /** 237 * Sets the roster item pre-approval state. 238 * 239 * @param approved the pre-approval flag. 240 */ 241 public void setApproved(boolean approved) { 242 this.approved = approved; 243 } 244 245 /** 246 * Returns an unmodifiable set of the group names that the roster item 247 * belongs to. 248 * 249 * @return an unmodifiable set of the group names. 250 */ 251 public Set<String> getGroupNames() { 252 return Collections.unmodifiableSet(groupNames); 253 } 254 255 /** 256 * Adds a group name. 257 * 258 * @param groupName the group name. 259 */ 260 public void addGroupName(String groupName) { 261 groupNames.add(groupName); 262 } 263 264 /** 265 * Removes a group name. 266 * 267 * @param groupName the group name. 268 */ 269 public void removeGroupName(String groupName) { 270 groupNames.remove(groupName); 271 } 272 273 @Override 274 public XmlStringBuilder toXML() { 275 XmlStringBuilder xml = new XmlStringBuilder(this); 276 xml.attribute("jid", jid); 277 xml.optAttribute("name", name); 278 xml.optAttribute("subscription", itemType); 279 if (subscriptionPending) { 280 xml.append(" ask='subscribe'"); 281 } 282 xml.optBooleanAttribute("approved", approved); 283 xml.rightAngleBracket(); 284 285 for (String groupName : groupNames) { 286 xml.openElement(GROUP).escape(groupName).closeElement(GROUP); 287 } 288 xml.closeElement(this); 289 return xml; 290 } 291 292 @Override 293 public int hashCode() { 294 final int prime = 31; 295 int result = 1; 296 result = prime * result + ((groupNames == null) ? 0 : groupNames.hashCode()); 297 result = prime * result + ((subscriptionPending) ? 0 : 1); 298 result = prime * result + ((itemType == null) ? 0 : itemType.hashCode()); 299 result = prime * result + ((name == null) ? 0 : name.hashCode()); 300 result = prime * result + ((jid == null) ? 0 : jid.hashCode()); 301 result = prime * result + ((approved == false) ? 0 : 1); 302 return result; 303 } 304 305 @Override 306 public boolean equals(Object obj) { 307 if (this == obj) 308 return true; 309 if (obj == null) 310 return false; 311 if (getClass() != obj.getClass()) 312 return false; 313 Item other = (Item) obj; 314 if (groupNames == null) { 315 if (other.groupNames != null) 316 return false; 317 } 318 else if (!groupNames.equals(other.groupNames)) 319 return false; 320 if (subscriptionPending != other.subscriptionPending) 321 return false; 322 if (itemType != other.itemType) 323 return false; 324 if (name == null) { 325 if (other.name != null) 326 return false; 327 } 328 else if (!name.equals(other.name)) 329 return false; 330 if (jid == null) { 331 if (other.jid != null) 332 return false; 333 } 334 else if (!jid.equals(other.jid)) 335 return false; 336 if (approved != other.approved) 337 return false; 338 return true; 339 } 340 341 } 342 343 public static enum ItemType { 344 345 /** 346 * The user does not have a subscription to the contact's presence, and the contact does not 347 * have a subscription to the user's presence; this is the default value, so if the 348 * subscription attribute is not included then the state is to be understood as "none". 349 */ 350 none('⊥'), 351 352 /** 353 * The user has a subscription to the contact's presence, but the contact does not have a 354 * subscription to the user's presence. 355 */ 356 to('←'), 357 358 /** 359 * The contact has a subscription to the user's presence, but the user does not have a 360 * subscription to the contact's presence. 361 */ 362 from('→'), 363 364 /** 365 * The user and the contact have subscriptions to each other's presence (also called a 366 * "mutual subscription"). 367 */ 368 both('↔'), 369 370 /** 371 * The user wishes to stop receiving presence updates from the subscriber. 372 */ 373 remove('⚡'), 374 ; 375 376 377 private static final char ME = '●'; 378 379 private final String symbol; 380 381 private ItemType(char secondSymbolChar) { 382 StringBuilder sb = new StringBuilder(2); 383 sb.append(ME).append(secondSymbolChar); 384 symbol = sb.toString(); 385 } 386 387 public static ItemType fromString(String string) { 388 if (StringUtils.isNullOrEmpty(string)) { 389 return none; 390 } 391 return ItemType.valueOf(string.toLowerCase(Locale.US)); 392 } 393 394 /** 395 * Get a String containing symbols representing the item type. The first symbol in the 396 * string is a big dot, representing the local entity. The second symbol represents the 397 * established subscription relation and is typically an arrow. The head(s) of the arrow 398 * point in the direction presence messages are sent. For example, if there is only a head 399 * pointing to the big dot, then the local user will receive presence information from the 400 * remote entity. 401 * 402 * @return the symbolic representation of this item type. 403 */ 404 public String asSymbol() { 405 return symbol; 406 } 407 } 408}