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