1 /* 2 * Copyright (C) 2004-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.roster; 18 19 import org.dom4j.io.SAXReader; 20 import org.jivesoftware.openfire.SharedGroupException; 21 import org.jivesoftware.openfire.group.Group; 22 import org.jivesoftware.openfire.group.GroupManager; 23 import org.jivesoftware.openfire.group.GroupNotFoundException; 24 import org.jivesoftware.openfire.user.UserNameManager; 25 import org.jivesoftware.openfire.user.UserNotFoundException; 26 import org.jivesoftware.util.cache.CacheSizes; 27 import org.jivesoftware.util.cache.Cacheable; 28 import org.jivesoftware.util.cache.CannotCalculateSizeException; 29 import org.jivesoftware.util.cache.ExternalizableUtil; 30 import org.xmpp.packet.JID; 31 import org.xmpp.packet.Presence; 32 33 import javax.xml.bind.Element; 34 import java.io.*; 35 import java.util.*; 36 37 /** 38 * <p>Represents a single roster item for a User's Roster.</p> 39 * <p>The server doesn't need to know anything about roster groups so they are 40 * not stored with easy retrieval or manipulation in mind. The important data 41 * elements of a roster item (beyond the jid adddress of the roster entry) includes:</p> 42 * <ul> 43 * <li>nick - A nickname for the user when used in this roster</li> 44 * <li>sub - A subscription type: to, from, none, both</li> 45 * <li>ask - An optional subscription ask status: subscribe, unsubscribe</li> 46 * <li>groups - A list of groups to organize roster entries under (e.g. friends, co-workers, etc)</li> 47 * </ul> 48 * 49 * @author Gaston Dombiak 50 */ 51 public class RosterItem implements Cacheable, Externalizable { 52 53 public enum SubType { 54 55 /** 56 * Indicates the roster item should be removed. 57 */ 58 REMOVE(-1), 59 /** 60 * No subscription is established. 61 */ 62 NONE(0), 63 /** 64 * The roster owner has a subscription to the roster item's presence. 65 */ 66 TO(1), 67 /** 68 * The roster item has a subscription to the roster owner's presence. 69 */ 70 FROM(2), 71 /** 72 * The roster item and owner have a mutual subscription. 73 */ 74 BOTH(3); 75 76 private final int value; 77 SubType(int value)78 SubType(int value) { 79 this.value = value; 80 } 81 getValue()82 public int getValue() { 83 return value; 84 } 85 getName()86 public String getName() { 87 return name().toLowerCase(); 88 } 89 getTypeFromInt(int value)90 public static SubType getTypeFromInt(int value) { 91 for (SubType subType : values()) { 92 if (subType.value == value) { 93 return subType; 94 } 95 } 96 return null; 97 } 98 } 99 100 public enum AskType { 101 102 /** 103 * The roster item has no pending subscription requests. 104 */ 105 NONE(-1), 106 /** 107 * The roster item has been asked for permission to subscribe to their presence 108 * but no response has been received. 109 */ 110 SUBSCRIBE(0), 111 /** 112 * The roster owner has asked to the roster item to unsubscribe from it's 113 * presence but has not received confirmation. 114 */ 115 UNSUBSCRIBE(1); 116 117 private final int value; 118 AskType(int value)119 AskType(int value) { 120 this.value = value; 121 122 } 123 getValue()124 public int getValue() { 125 return value; 126 } 127 getTypeFromInt(int value)128 public static AskType getTypeFromInt(int value) { 129 for (AskType askType : values()) { 130 if (askType.value == value) { 131 return askType; 132 } 133 } 134 return null; 135 } 136 } 137 138 public enum RecvType { 139 140 /** 141 * There are no subscriptions that have been received but not presented to the user. 142 */ 143 NONE(-1), 144 /** 145 * The server has received a subscribe request, but has not forwarded it to the user. 146 */ 147 SUBSCRIBE(1), 148 /** 149 * The server has received an unsubscribe request, but has not forwarded it to the user. 150 */ 151 UNSUBSCRIBE(2); 152 153 private final int value; 154 RecvType(int value)155 RecvType(int value) { 156 this.value = value; 157 } 158 getValue()159 public int getValue() { 160 return value; 161 } 162 getTypeFromInt(int value)163 public static RecvType getTypeFromInt(int value) { 164 for (RecvType recvType : values()) { 165 if (recvType.value == value) { 166 return recvType; 167 } 168 } 169 return null; 170 } 171 } 172 173 /** 174 * <p>Indicates the roster item should be removed.</p> 175 */ 176 public static final SubType SUB_REMOVE = SubType.REMOVE; 177 /** 178 * <p>No subscription is established.</p> 179 */ 180 public static final SubType SUB_NONE = SubType.NONE; 181 /** 182 * <p>The roster owner has a subscription to the roster item's presence.</p> 183 */ 184 public static final SubType SUB_TO = SubType.TO; 185 /** 186 * <p>The roster item has a subscription to the roster owner's presence.</p> 187 */ 188 public static final SubType SUB_FROM = SubType.FROM; 189 /** 190 * <p>The roster item and owner have a mutual subscription.</p> 191 */ 192 public static final SubType SUB_BOTH = SubType.BOTH; 193 194 /** 195 * <p>The roster item has no pending subscription requests.</p> 196 */ 197 public static final AskType ASK_NONE = AskType.NONE; 198 /** 199 * <p>The roster item has been asked for permission to subscribe to their presence 200 * but no response has been received.</p> 201 */ 202 public static final AskType ASK_SUBSCRIBE = AskType.SUBSCRIBE; 203 /** 204 * <p>The roster owner has asked to the roster item to unsubscribe from it's 205 * presence but has not received confirmation.</p> 206 */ 207 public static final AskType ASK_UNSUBSCRIBE = AskType.UNSUBSCRIBE; 208 209 /** 210 * <p>There are no subscriptions that have been received but not presented to the user.</p> 211 */ 212 public static final RecvType RECV_NONE = RecvType.NONE; 213 /** 214 * <p>The server has received a subscribe request, but has not forwarded it to the user.</p> 215 */ 216 public static final RecvType RECV_SUBSCRIBE = RecvType.SUBSCRIBE; 217 /** 218 * <p>The server has received an unsubscribe request, but has not forwarded it to the user.</p> 219 */ 220 public static final RecvType RECV_UNSUBSCRIBE = RecvType.UNSUBSCRIBE; 221 222 protected RecvType recvStatus; 223 protected JID jid; 224 protected String nickname; 225 protected List<String> groups; 226 protected Set<String> sharedGroups = new HashSet<>(); 227 protected Set<String> invisibleSharedGroups = new HashSet<>(); 228 protected SubType subStatus; 229 protected AskType askStatus; 230 // Presence for type='subscribe'. 231 protected Presence subscribeStanza; 232 /** 233 * Holds the ID that uniquely identifies the roster in the backend store. A value of 234 * zero means that the roster item is not persistent. 235 */ 236 private long rosterID; 237 238 /** 239 * Constructor added for Externalizable. Do not use this constructor. 240 */ RosterItem()241 public RosterItem() { 242 } 243 RosterItem(long id, JID jid, SubType subStatus, AskType askStatus, RecvType recvStatus, String nickname, List<String> groups)244 public RosterItem(long id, 245 JID jid, 246 SubType subStatus, 247 AskType askStatus, 248 RecvType recvStatus, 249 String nickname, 250 List<String> groups) { 251 this(jid, subStatus, askStatus, recvStatus, nickname, groups); 252 this.rosterID = id; 253 } 254 RosterItem(JID jid, SubType subStatus, AskType askStatus, RecvType recvStatus, String nickname, List<String> groups)255 public RosterItem(JID jid, 256 SubType subStatus, 257 AskType askStatus, 258 RecvType recvStatus, 259 String nickname, 260 List<String> groups) { 261 this.jid = jid; 262 this.subStatus = subStatus; 263 this.askStatus = askStatus; 264 this.recvStatus = recvStatus; 265 this.nickname = nickname; 266 this.groups = new LinkedList<>(); 267 if (groups != null) { 268 for (String group : groups) { 269 this.groups.add(group); 270 } 271 } 272 } 273 274 /** 275 * Create a roster item from the data in another one. 276 * 277 * @param item Item that contains the info of the roster item. 278 */ RosterItem(org.xmpp.packet.Roster.Item item)279 public RosterItem(org.xmpp.packet.Roster.Item item) { 280 this(item.getJID(), 281 getSubType(item), 282 getAskStatus(item), 283 RosterItem.RECV_NONE, 284 item.getName(), 285 new LinkedList<>(item.getGroups())); 286 } 287 getAskStatus(org.xmpp.packet.Roster.Item item)288 public static RosterItem.AskType getAskStatus(org.xmpp.packet.Roster.Item item) { 289 if (item.getAsk() == org.xmpp.packet.Roster.Ask.subscribe) { 290 return RosterItem.ASK_SUBSCRIBE; 291 } 292 else if (item.getAsk() == org.xmpp.packet.Roster.Ask.unsubscribe) { 293 return RosterItem.ASK_UNSUBSCRIBE; 294 } 295 else { 296 return RosterItem.ASK_NONE; 297 } 298 } 299 getSubType(org.xmpp.packet.Roster.Item item)300 public static RosterItem.SubType getSubType(org.xmpp.packet.Roster.Item item) { 301 if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.to) { 302 return RosterItem.SUB_TO; 303 } 304 else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.from) { 305 return RosterItem.SUB_FROM; 306 } 307 else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.both) { 308 return RosterItem.SUB_BOTH; 309 } 310 else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.remove) { 311 return RosterItem.SUB_REMOVE; 312 } 313 else { 314 return RosterItem.SUB_NONE; 315 } 316 } 317 318 /** 319 * <p>Obtain the current subscription status of the item.</p> 320 * 321 * @return The subscription status of the item 322 */ getSubStatus()323 public SubType getSubStatus() { 324 return subStatus; 325 } 326 327 /** 328 * <p>Set the current subscription status of the item.</p> 329 * 330 * @param subStatus The subscription status of the item 331 */ setSubStatus(SubType subStatus)332 public void setSubStatus(SubType subStatus) { 333 // Optimization: Load user only if we need to set the nickname of the roster item 334 if ("".equals(nickname) && (subStatus == SUB_BOTH || subStatus == SUB_TO)) { 335 try { 336 nickname = UserNameManager.getUserName(jid); 337 } 338 catch (UserNotFoundException e) { 339 // Do nothing 340 } 341 } 342 this.subStatus = subStatus; 343 } 344 345 /** 346 * <p>Obtain the current ask status of the item.</p> 347 * 348 * @return The ask status of the item 349 */ getAskStatus()350 public AskType getAskStatus() { 351 if (isShared()) { 352 // Redefine the ask status since the item belongs to a shared group 353 return ASK_NONE; 354 } 355 else { 356 return askStatus; 357 } 358 } 359 360 /** 361 * <p>Set the current ask status of the item.</p> 362 * 363 * @param askStatus The ask status of the item 364 */ setAskStatus(AskType askStatus)365 public void setAskStatus(AskType askStatus) { 366 this.askStatus = askStatus; 367 } 368 369 /** 370 * <p>Obtain the current recv status of the item.</p> 371 * 372 * @return The recv status of the item 373 */ getRecvStatus()374 public RecvType getRecvStatus() { 375 return recvStatus; 376 } 377 378 /** 379 * <p>Set the current recv status of the item.</p> 380 * 381 * @param recvStatus The recv status of the item 382 */ setRecvStatus(RecvType recvStatus)383 public void setRecvStatus(RecvType recvStatus) { 384 this.recvStatus = recvStatus; 385 } 386 387 /** 388 * <p>Obtain the address of the item.</p> 389 * 390 * @return The address of the item 391 */ getJid()392 public JID getJid() { 393 return jid; 394 } 395 396 /** 397 * <p>Obtain the current nickname for the item.</p> 398 * 399 * @return The subscription status of the item 400 */ getNickname()401 public String getNickname() { 402 return nickname; 403 } 404 405 /** 406 * <p>Set the current nickname for the item.</p> 407 * 408 * @param nickname The subscription status of the item 409 */ setNickname(String nickname)410 public void setNickname(String nickname) { 411 this.nickname = nickname; 412 } 413 414 /** 415 * Returns the groups for the item. Shared groups won't be included in the answer. 416 * 417 * @return The groups for the item. 418 */ getGroups()419 public List<String> getGroups() { 420 return groups; 421 } 422 423 /** 424 * Set the current groups for the item. 425 * 426 * @param groups The new lists of groups the item belongs to. 427 * @throws org.jivesoftware.openfire.SharedGroupException if trying to remove shared group. 428 */ setGroups(List<String> groups)429 public void setGroups(List<String> groups) throws SharedGroupException { 430 if (groups == null) { 431 this.groups = new LinkedList<>(); 432 } 433 else { 434 // Raise an error if the user is trying to remove the item from a shared group 435 for (Group group: getSharedGroups()) { 436 // Get the display name of the group 437 String groupName = group.getProperties().get("sharedRoster.displayName"); 438 // Check if the group has been removed from the new groups list 439 if (!groups.contains(groupName)) { 440 throw new SharedGroupException("Cannot remove item from shared group"); 441 } 442 } 443 444 // Remove shared groups from the param 445 for (Iterator<String> it=groups.iterator(); it.hasNext();) { 446 String groupName = it.next(); 447 try { 448 Group group = GroupManager.getInstance().getGroup(groupName); 449 if (RosterManager.isSharedGroup(group)) { 450 it.remove(); 451 } 452 } catch (GroupNotFoundException e) { 453 // Check now if there is a group whose display name matches the requested group 454 Collection<Group> groupsWithProp = GroupManager 455 .getInstance() 456 .search("sharedRoster.displayName", groupName); 457 Iterator<Group> itr = groupsWithProp.iterator(); 458 while(itr.hasNext()) { 459 Group group = itr.next(); 460 if (RosterManager.isSharedGroup(group)) { 461 it.remove(); 462 } 463 } 464 } 465 } 466 this.groups = groups; 467 } 468 } 469 470 /** 471 * Returns the shared groups for the item. 472 * 473 * @return The shared groups this item belongs to. 474 */ getSharedGroups()475 public Collection<Group> getSharedGroups() { 476 Collection<Group> groups = new ArrayList<>(sharedGroups.size()); 477 for (String groupName : sharedGroups) { 478 try { 479 groups.add(GroupManager.getInstance().getGroup(groupName)); 480 } 481 catch (GroupNotFoundException e) { 482 // Do nothing 483 } 484 } 485 return groups; 486 } 487 488 /** 489 * Returns the invisible shared groups for the item. These groups are for internal use 490 * and help track the reason why a roster item has a presence subscription of type FROM 491 * when using shared groups. 492 * 493 * @return The shared groups this item belongs to. 494 */ getInvisibleSharedGroups()495 public Collection<Group> getInvisibleSharedGroups() { 496 Collection<Group> groups = new ArrayList<>(invisibleSharedGroups.size()); 497 for (String groupName : invisibleSharedGroups) { 498 try { 499 groups.add(GroupManager.getInstance().getGroup(groupName)); 500 } 501 catch (GroupNotFoundException e) { 502 // Do nothing 503 } 504 } 505 return groups; 506 } 507 getInvisibleSharedGroupsNames()508 Set<String> getInvisibleSharedGroupsNames() { 509 return invisibleSharedGroups; 510 } 511 setInvisibleSharedGroupsNames(Set<String> groupsNames)512 void setInvisibleSharedGroupsNames(Set<String> groupsNames) { 513 invisibleSharedGroups = groupsNames; 514 } 515 516 /** 517 * Adds a new group to the shared groups list. 518 * 519 * @param sharedGroup The shared group to add to the list of shared groups. 520 */ addSharedGroup(Group sharedGroup)521 public void addSharedGroup(Group sharedGroup) { 522 sharedGroups.add(sharedGroup.getName()); 523 invisibleSharedGroups.remove(sharedGroup.getName()); 524 } 525 526 /** 527 * Adds a new group to the list shared groups that won't be sent to the user. These groups 528 * are for internal use and help track the reason why a roster item has a presence 529 * subscription of type FROM when using shared groups. 530 * 531 * @param sharedGroup The shared group to add to the list of shared groups. 532 */ addInvisibleSharedGroup(Group sharedGroup)533 public void addInvisibleSharedGroup(Group sharedGroup) { 534 invisibleSharedGroups.add(sharedGroup.getName()); 535 } 536 537 /** 538 * Removes a group from the shared groups list. 539 * 540 * @param sharedGroup The shared group to remove from the list of shared groups. 541 */ removeSharedGroup(Group sharedGroup)542 public void removeSharedGroup(Group sharedGroup) { 543 sharedGroups.remove(sharedGroup.getName()); 544 invisibleSharedGroups.remove(sharedGroup.getName()); 545 } 546 547 /** 548 * Returns true if this item belongs to a shared group. Return true even if the item belongs 549 * to a personal group and a shared group. 550 * 551 * @return true if this item belongs to a shared group. 552 */ isShared()553 public boolean isShared() { 554 return !sharedGroups.isEmpty() || !invisibleSharedGroups.isEmpty(); 555 } 556 557 /** 558 * Returns true if this item belongs ONLY to shared groups. This means that the the item is 559 * considered to be "only shared" if it doesn't belong to a personal group but only to shared 560 * groups. 561 * 562 * @return true if this item belongs ONLY to shared groups. 563 */ isOnlyShared()564 public boolean isOnlyShared() { 565 return isShared() && groups.isEmpty(); 566 } 567 568 /** 569 * Returns the roster ID associated with this particular roster item. A value of zero 570 * means that the roster item is not being persisted in the backend store.<p> 571 * 572 * Databases can use the roster ID as the key in locating roster items. 573 * 574 * @return The roster ID 575 */ getID()576 public long getID() { 577 return rosterID; 578 } 579 580 /** 581 * Sets the roster ID associated with this particular roster item. A value of zero 582 * means that the roster item is not being persisted in the backend store.<p> 583 * 584 * Databases can use the roster ID as the key in locating roster items. 585 * 586 * @param rosterID The roster ID. 587 */ setID(long rosterID)588 public void setID(long rosterID) { 589 this.rosterID = rosterID; 590 } 591 592 /** 593 * <p>Update the cached item as a copy of the given item.</p> 594 * <p>A convenience for getting the item and setting each attribute.</p> 595 * 596 * @param item The item who's settings will be copied into the cached copy 597 * @throws org.jivesoftware.openfire.SharedGroupException if trying to remove shared group. 598 */ setAsCopyOf(org.xmpp.packet.Roster.Item item)599 public void setAsCopyOf(org.xmpp.packet.Roster.Item item) throws SharedGroupException { 600 setGroups(new LinkedList<>(item.getGroups())); 601 setNickname(item.getName()); 602 } 603 604 /* 605 * (non-Javadoc) 606 * 607 * @see org.jivesoftware.util.cache.Cacheable#getCachedSize() 608 */ 609 @Override getCachedSize()610 public int getCachedSize() throws CannotCalculateSizeException { 611 int size = jid.toBareJID().length(); 612 size += CacheSizes.sizeOfString(nickname); 613 size += CacheSizes.sizeOfCollection(groups); 614 size += CacheSizes.sizeOfCollection(invisibleSharedGroups); 615 size += CacheSizes.sizeOfCollection(sharedGroups); 616 size += CacheSizes.sizeOfInt(); // subStatus 617 size += CacheSizes.sizeOfInt(); // askStatus 618 size += CacheSizes.sizeOfInt(); // recvStatus 619 size += CacheSizes.sizeOfLong(); // id 620 if (subscribeStanza != null) { 621 size += CacheSizes.sizeOfString(subscribeStanza.toXML()); 622 } 623 return size; 624 } 625 626 @Override writeExternal(ObjectOutput out)627 public void writeExternal(ObjectOutput out) throws IOException { 628 ExternalizableUtil.getInstance().writeSerializable(out, jid); 629 ExternalizableUtil.getInstance().writeBoolean(out, nickname != null); 630 if (nickname != null) { 631 ExternalizableUtil.getInstance().writeSafeUTF(out, nickname); 632 } 633 ExternalizableUtil.getInstance().writeStrings(out, groups); 634 ExternalizableUtil.getInstance().writeStrings(out, sharedGroups); 635 ExternalizableUtil.getInstance().writeStrings(out, invisibleSharedGroups); 636 ExternalizableUtil.getInstance().writeInt(out, recvStatus.getValue()); 637 ExternalizableUtil.getInstance().writeInt(out, subStatus.getValue()); 638 ExternalizableUtil.getInstance().writeInt(out, askStatus.getValue()); 639 ExternalizableUtil.getInstance().writeLong(out, rosterID); 640 ExternalizableUtil.getInstance().writeBoolean(out, subscribeStanza != null); 641 if (subscribeStanza != null) { 642 ExternalizableUtil.getInstance().writeXML(out, subscribeStanza.getElement()); 643 } 644 } 645 646 @Override readExternal(ObjectInput in)647 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 648 jid = (JID) ExternalizableUtil.getInstance().readSerializable(in); 649 if (ExternalizableUtil.getInstance().readBoolean(in)) { 650 nickname = ExternalizableUtil.getInstance().readSafeUTF(in); 651 } 652 this.groups = new LinkedList<>(); 653 ExternalizableUtil.getInstance().readStrings(in, groups); 654 ExternalizableUtil.getInstance().readStrings(in, sharedGroups); 655 ExternalizableUtil.getInstance().readStrings(in, invisibleSharedGroups); 656 recvStatus = RecvType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in)); 657 subStatus = SubType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in)); 658 askStatus = AskType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in)); 659 rosterID = ExternalizableUtil.getInstance().readLong(in); 660 if (ExternalizableUtil.getInstance().readBoolean(in)) { 661 subscribeStanza = new Presence(ExternalizableUtil.getInstance().readXML(in)); 662 } 663 } 664 getSubscribeStanza()665 public Presence getSubscribeStanza() throws IllegalStateException { 666 if (recvStatus != RecvType.SUBSCRIBE) { 667 throw new IllegalStateException("Wrong receive state"); 668 } 669 if (subscribeStanza == null) { 670 Presence presence = new Presence(); 671 presence.setFrom(jid); 672 presence.setType(Presence.Type.subscribe); 673 return presence; 674 } else { 675 return subscribeStanza; 676 } 677 } 678 getStoredSubscribeStanza()679 public Presence getStoredSubscribeStanza() { 680 return subscribeStanza; 681 } 682 setStoredSubscribeStanza(Presence subscribeStanza)683 public void setStoredSubscribeStanza(Presence subscribeStanza) { 684 this.subscribeStanza = subscribeStanza; 685 } 686 } 687