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