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