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; 019 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Iterator; 023import java.util.List; 024 025import org.jivesoftware.smack.Manager; 026import org.jivesoftware.smack.SmackException.NoResponseException; 027import org.jivesoftware.smack.SmackException.NotConnectedException; 028import org.jivesoftware.smack.XMPPConnection; 029import org.jivesoftware.smack.XMPPException.XMPPErrorException; 030import org.jivesoftware.smack.packet.IQ; 031import org.jivesoftware.smack.packet.Presence; 032import org.jivesoftware.smack.packet.Presence.Type; 033import org.jivesoftware.smack.roster.packet.RosterPacket; 034import org.jivesoftware.smack.util.EqualsUtil; 035 036import org.jxmpp.jid.BareJid; 037 038 039/** 040 * Each user in your roster is represented by a roster entry, which contains the user's 041 * JID and a name or nickname you assign. 042 * 043 * @author Matt Tucker 044 * @author Florian Schmaus 045 */ 046public final class RosterEntry extends Manager { 047 048 private RosterPacket.Item item; 049 private final Roster roster; 050 051 /** 052 * Creates a new roster entry. 053 * 054 * @param item the Roster Stanza's Item entry. 055 * @param roster The Roster managing this entry. 056 * @param connection a connection to the XMPP server. 057 */ 058 RosterEntry(RosterPacket.Item item, Roster roster, XMPPConnection connection) { 059 super(connection); 060 this.item = item; 061 this.roster = roster; 062 } 063 064 /** 065 * Returns the JID of the user associated with this entry. 066 * 067 * @return the user associated with this entry. 068 * @deprecated use {@link #getJid()} instead. 069 */ 070 @Deprecated 071 public String getUser() { 072 return getJid().toString(); 073 } 074 075 /** 076 * Returns the JID associated with this entry. 077 * 078 * @return the user associated with this entry. 079 */ 080 public BareJid getJid() { 081 return item.getJid(); 082 } 083 084 /** 085 * Returns the name associated with this entry. 086 * 087 * @return the name. 088 */ 089 public String getName() { 090 return item.getName(); 091 } 092 093 /** 094 * Sets the name associated with this entry. 095 * 096 * @param name the name. 097 * @throws NotConnectedException if the XMPP connection is not connected. 098 * @throws XMPPErrorException if there was an XMPP error returned. 099 * @throws NoResponseException if there was no response from the remote entity. 100 * @throws InterruptedException if the calling thread was interrupted. 101 */ 102 public synchronized void setName(String name) throws NotConnectedException, NoResponseException, XMPPErrorException, InterruptedException { 103 // Do nothing if the name hasn't changed. 104 if (name != null && name.equals(getName())) { 105 return; 106 } 107 108 RosterPacket packet = new RosterPacket(); 109 packet.setType(IQ.Type.set); 110 111 // Create a new roster item with the current RosterEntry and the *new* name. Note that we can't set the name of 112 // RosterEntry right away, as otherwise the updated event wont get fired, because equalsDeep would return true. 113 packet.addRosterItem(toRosterItem(this, name)); 114 connection().createStanzaCollectorAndSend(packet).nextResultOrThrow(); 115 116 // We have received a result response to the IQ set, the name was successfully changed 117 item.setName(name); 118 } 119 120 /** 121 * Updates this entries item. 122 * 123 * @param item new item 124 */ 125 void updateItem(RosterPacket.Item item) { 126 assert item != null; 127 this.item = item; 128 } 129 130 /** 131 * Returns the pre-approval state of this entry. 132 * 133 * @return the pre-approval state. 134 */ 135 public boolean isApproved() { 136 return item.isApproved(); 137 } 138 139 /** 140 * Returns an copied list of the roster groups that this entry belongs to. 141 * 142 * @return an iterator for the groups this entry belongs to. 143 */ 144 public List<RosterGroup> getGroups() { 145 List<RosterGroup> results = new ArrayList<>(); 146 // Loop through all roster groups and find the ones that contain this 147 // entry. This algorithm should be fine 148 for (RosterGroup group : roster.getGroups()) { 149 if (group.contains(this)) { 150 results.add(group); 151 } 152 } 153 return results; 154 } 155 156 /** 157 * Returns the roster subscription type of the entry. When the type is 158 * RosterPacket.ItemType.none or RosterPacket.ItemType.from, 159 * refer to {@link RosterEntry getStatus()} to see if a subscription request 160 * is pending. 161 * 162 * @return the type. 163 */ 164 public RosterPacket.ItemType getType() { 165 return item.getItemType(); 166 } 167 168 /** 169 * Returns the roster subscription request status of the entry. If 170 * {@code true}, then the contact did not answer the subscription request 171 * yet. 172 * 173 * @return the status. 174 * @since 4.2 175 */ 176 public boolean isSubscriptionPending() { 177 return item.isSubscriptionPending(); 178 } 179 180 /** 181 * Check if the contact is subscribed to "my" presence. This allows the contact to see the presence information. 182 * 183 * @return true if the contact has a presence subscription. 184 * @since 4.2 185 */ 186 public boolean canSeeMyPresence() { 187 switch (getType()) { 188 case from: 189 case both: 190 return true; 191 default: 192 return false; 193 } 194 } 195 196 /** 197 * Check if we are subscribed to the contact's presence. If <code>true</code> then the contact has allowed us to 198 * receive presence information. 199 * 200 * @return true if we are subscribed to the contact's presence. 201 * @since 4.2 202 */ 203 public boolean canSeeHisPresence() { 204 switch (getType()) { 205 case to: 206 case both: 207 return true; 208 default: 209 return false; 210 } 211 } 212 213 /** 214 * Cancel the presence subscription the XMPP entity representing this roster entry has with us. 215 * 216 * @throws NotConnectedException if the XMPP connection is not connected. 217 * @throws InterruptedException if the calling thread was interrupted. 218 * @since 4.2 219 */ 220 public void cancelSubscription() throws NotConnectedException, InterruptedException { 221 XMPPConnection connection = connection(); 222 Presence unsubscribed = connection.getStanzaFactory().buildPresenceStanza() 223 .to(item.getJid()) 224 .ofType(Type.unsubscribed) 225 .build(); 226 connection.sendStanza(unsubscribed); 227 } 228 229 @Override 230 public String toString() { 231 StringBuilder buf = new StringBuilder(); 232 if (getName() != null) { 233 buf.append(getName()).append(": "); 234 } 235 buf.append(getJid()); 236 Collection<RosterGroup> groups = getGroups(); 237 if (!groups.isEmpty()) { 238 buf.append(" ["); 239 Iterator<RosterGroup> iter = groups.iterator(); 240 RosterGroup group = iter.next(); 241 buf.append(group.getName()); 242 while (iter.hasNext()) { 243 buf.append(", "); 244 group = iter.next(); 245 buf.append(group.getName()); 246 } 247 buf.append(']'); 248 } 249 return buf.toString(); 250 } 251 252 @Override 253 public int hashCode() { 254 return getJid().hashCode(); 255 } 256 257 @Override 258 public boolean equals(Object object) { 259 return EqualsUtil.equals(this, object, (e, o) -> 260 e.append(getJid(), o.getJid()) 261 ); 262 } 263 264 /** 265 * Indicates whether some other object is "equal to" this by comparing all members. 266 * <p> 267 * The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal. 268 * 269 * @param obj the reference object with which to compare. 270 * @return <code>true</code> if this object is the same as the obj argument; <code>false</code> 271 * otherwise. 272 */ 273 public boolean equalsDeep(Object obj) { 274 return EqualsUtil.equals(this, obj, (e, o) -> 275 e.append(item, o.item) 276 ); 277 } 278 279 /** 280 * Convert the RosterEntry to a Roster stanza <item/> element. 281 * 282 * @param entry the roster entry. 283 * @return the roster item. 284 */ 285 static RosterPacket.Item toRosterItem(RosterEntry entry) { 286 return toRosterItem(entry, entry.getName(), false); 287 } 288 289 /** 290 * Convert the RosterEntry to a Roster stanza <item/> element. 291 * 292 * @param entry the roster entry 293 * @param name the name of the roster item. 294 * @return the roster item. 295 */ 296 static RosterPacket.Item toRosterItem(RosterEntry entry, String name) { 297 return toRosterItem(entry, name, false); 298 } 299 300 static RosterPacket.Item toRosterItem(RosterEntry entry, boolean includeAskAttribute) { 301 return toRosterItem(entry, entry.getName(), includeAskAttribute); 302 } 303 304 /** 305 * Convert a roster entry with the given name to a roster item. As per RFC 6121 ยง 2.1.2.2., clients MUST NOT include 306 * the 'ask' attribute, thus set {@code includeAskAttribute} to {@code false}. 307 * 308 * @param entry the roster entry. 309 * @param name the name of the roster item. 310 * @param includeAskAttribute whether or not to include the 'ask' attribute. 311 * @return the roster item. 312 */ 313 private static RosterPacket.Item toRosterItem(RosterEntry entry, String name, boolean includeAskAttribute) { 314 RosterPacket.Item item = new RosterPacket.Item(entry.getJid(), name); 315 item.setItemType(entry.getType()); 316 if (includeAskAttribute) { 317 item.setSubscriptionPending(entry.isSubscriptionPending()); 318 } 319 item.setApproved(entry.isApproved()); 320 // Set the correct group names for the item. 321 for (RosterGroup group : entry.getGroups()) { 322 item.addGroupName(group.getName()); 323 } 324 return item; 325 } 326 327}