/* * Copyright (C) 2004-2008 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.openfire.roster; import org.jivesoftware.openfire.SharedGroupException; import org.jivesoftware.openfire.group.Group; import org.jivesoftware.openfire.group.GroupManager; import org.jivesoftware.openfire.group.GroupNotFoundException; import org.jivesoftware.openfire.user.UserNameManager; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.cache.CacheSizes; import org.jivesoftware.util.cache.Cacheable; import org.jivesoftware.util.cache.CannotCalculateSizeException; import org.jivesoftware.util.cache.ExternalizableUtil; import org.xmpp.packet.JID; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.*; /** * <p>Represents a single roster item for a User's Roster.</p> * <p>The server doesn't need to know anything about roster groups so they are * not stored with easy retrieval or manipulation in mind. The important data * elements of a roster item (beyond the jid adddress of the roster entry) includes:</p> * <ul> * <li>nick - A nickname for the user when used in this roster</li> * <li>sub - A subscription type: to, from, none, both</li> * <li>ask - An optional subscription ask status: subscribe, unsubscribe</li> * <li>groups - A list of groups to organize roster entries under (e.g. friends, co-workers, etc)</li> * </ul> * * @author Gaston Dombiak */ public class RosterItem implements Cacheable, Externalizable { public enum SubType { /** * Indicates the roster item should be removed. */ REMOVE(-1), /** * No subscription is established. */ NONE(0), /** * The roster owner has a subscription to the roster item's presence. */ TO(1), /** * The roster item has a subscription to the roster owner's presence. */ FROM(2), /** * The roster item and owner have a mutual subscription. */ BOTH(3); private final int value; SubType(int value) { this.value = value; } public int getValue() { return value; } public String getName() { return name().toLowerCase(); } public static SubType getTypeFromInt(int value) { for (SubType subType : values()) { if (subType.value == value) { return subType; } } return null; } } public enum AskType { /** * The roster item has no pending subscription requests. */ NONE(-1), /** * The roster item has been asked for permission to subscribe to their presence * but no response has been received. */ SUBSCRIBE(0), /** * The roster owner has asked to the roster item to unsubscribe from it's * presence but has not received confirmation. */ UNSUBSCRIBE(1); private final int value; AskType(int value) { this.value = value; } public int getValue() { return value; } public static AskType getTypeFromInt(int value) { for (AskType askType : values()) { if (askType.value == value) { return askType; } } return null; } } public enum RecvType { /** * There are no subscriptions that have been received but not presented to the user. */ NONE(-1), /** * The server has received a subscribe request, but has not forwarded it to the user. */ SUBSCRIBE(1), /** * The server has received an unsubscribe request, but has not forwarded it to the user. */ UNSUBSCRIBE(2); private final int value; RecvType(int value) { this.value = value; } public int getValue() { return value; } public static RecvType getTypeFromInt(int value) { for (RecvType recvType : values()) { if (recvType.value == value) { return recvType; } } return null; } } /** * <p>Indicates the roster item should be removed.</p> */ public static final SubType SUB_REMOVE = SubType.REMOVE; /** * <p>No subscription is established.</p> */ public static final SubType SUB_NONE = SubType.NONE; /** * <p>The roster owner has a subscription to the roster item's presence.</p> */ public static final SubType SUB_TO = SubType.TO; /** * <p>The roster item has a subscription to the roster owner's presence.</p> */ public static final SubType SUB_FROM = SubType.FROM; /** * <p>The roster item and owner have a mutual subscription.</p> */ public static final SubType SUB_BOTH = SubType.BOTH; /** * <p>The roster item has no pending subscription requests.</p> */ public static final AskType ASK_NONE = AskType.NONE; /** * <p>The roster item has been asked for permission to subscribe to their presence * but no response has been received.</p> */ public static final AskType ASK_SUBSCRIBE = AskType.SUBSCRIBE; /** * <p>The roster owner has asked to the roster item to unsubscribe from it's * presence but has not received confirmation.</p> */ public static final AskType ASK_UNSUBSCRIBE = AskType.UNSUBSCRIBE; /** * <p>There are no subscriptions that have been received but not presented to the user.</p> */ public static final RecvType RECV_NONE = RecvType.NONE; /** * <p>The server has received a subscribe request, but has not forwarded it to the user.</p> */ public static final RecvType RECV_SUBSCRIBE = RecvType.SUBSCRIBE; /** * <p>The server has received an unsubscribe request, but has not forwarded it to the user.</p> */ public static final RecvType RECV_UNSUBSCRIBE = RecvType.UNSUBSCRIBE; protected RecvType recvStatus; protected JID jid; protected String nickname; protected List<String> groups; protected Set<String> sharedGroups = new HashSet<>(); protected Set<String> invisibleSharedGroups = new HashSet<>(); protected SubType subStatus; protected AskType askStatus; /** * Holds the ID that uniquely identifies the roster in the backend store. A value of * zero means that the roster item is not persistent. */ private long rosterID; /** * Constructor added for Externalizable. Do not use this constructor. */ public RosterItem() { } public RosterItem(long id, JID jid, SubType subStatus, AskType askStatus, RecvType recvStatus, String nickname, List<String> groups) { this(jid, subStatus, askStatus, recvStatus, nickname, groups); this.rosterID = id; } public RosterItem(JID jid, SubType subStatus, AskType askStatus, RecvType recvStatus, String nickname, List<String> groups) { this.jid = jid; this.subStatus = subStatus; this.askStatus = askStatus; this.recvStatus = recvStatus; this.nickname = nickname; this.groups = new LinkedList<>(); if (groups != null) { for (String group : groups) { this.groups.add(group); } } } /** * Create a roster item from the data in another one. * * @param item Item that contains the info of the roster item. */ public RosterItem(org.xmpp.packet.Roster.Item item) { this(item.getJID(), getSubType(item), getAskStatus(item), RosterItem.RECV_NONE, item.getName(), new LinkedList<>(item.getGroups())); } private static RosterItem.AskType getAskStatus(org.xmpp.packet.Roster.Item item) { if (item.getAsk() == org.xmpp.packet.Roster.Ask.subscribe) { return RosterItem.ASK_SUBSCRIBE; } else if (item.getAsk() == org.xmpp.packet.Roster.Ask.unsubscribe) { return RosterItem.ASK_UNSUBSCRIBE; } else { return RosterItem.ASK_NONE; } } private static RosterItem.SubType getSubType(org.xmpp.packet.Roster.Item item) { if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.to) { return RosterItem.SUB_TO; } else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.from) { return RosterItem.SUB_FROM; } else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.both) { return RosterItem.SUB_BOTH; } else if (item.getSubscription() == org.xmpp.packet.Roster.Subscription.remove) { return RosterItem.SUB_REMOVE; } else { return RosterItem.SUB_NONE; } } /** * <p>Obtain the current subscription status of the item.</p> * * @return The subscription status of the item */ public SubType getSubStatus() { return subStatus; } /** * <p>Set the current subscription status of the item.</p> * * @param subStatus The subscription status of the item */ public void setSubStatus(SubType subStatus) { // Optimization: Load user only if we need to set the nickname of the roster item if ("".equals(nickname) && (subStatus == SUB_BOTH || subStatus == SUB_TO)) { try { nickname = UserNameManager.getUserName(jid); } catch (UserNotFoundException e) { // Do nothing } } this.subStatus = subStatus; } /** * <p>Obtain the current ask status of the item.</p> * * @return The ask status of the item */ public AskType getAskStatus() { if (isShared()) { // Redefine the ask status since the item belongs to a shared group return ASK_NONE; } else { return askStatus; } } /** * <p>Set the current ask status of the item.</p> * * @param askStatus The ask status of the item */ public void setAskStatus(AskType askStatus) { this.askStatus = askStatus; } /** * <p>Obtain the current recv status of the item.</p> * * @return The recv status of the item */ public RecvType getRecvStatus() { return recvStatus; } /** * <p>Set the current recv status of the item.</p> * * @param recvStatus The recv status of the item */ public void setRecvStatus(RecvType recvStatus) { this.recvStatus = recvStatus; } /** * <p>Obtain the address of the item.</p> * * @return The address of the item */ public JID getJid() { return jid; } /** * <p>Obtain the current nickname for the item.</p> * * @return The subscription status of the item */ public String getNickname() { return nickname; } /** * <p>Set the current nickname for the item.</p> * * @param nickname The subscription status of the item */ public void setNickname(String nickname) { this.nickname = nickname; } /** * Returns the groups for the item. Shared groups won't be included in the answer. * * @return The groups for the item. */ public List<String> getGroups() { return groups; } /** * Set the current groups for the item. * * @param groups The new lists of groups the item belongs to. * @throws org.jivesoftware.openfire.SharedGroupException if trying to remove shared group. */ public void setGroups(List<String> groups) throws SharedGroupException { if (groups == null) { this.groups = new LinkedList<>(); } else { // Raise an error if the user is trying to remove the item from a shared group for (Group group: getSharedGroups()) { // Get the display name of the group String groupName = group.getProperties().get("sharedRoster.displayName"); // Check if the group has been removed from the new groups list if (!groups.contains(groupName)) { throw new SharedGroupException("Cannot remove item from shared group"); } } // Remove shared groups from the param for (Iterator<String> it=groups.iterator(); it.hasNext();) { String groupName = it.next(); try { Group group = GroupManager.getInstance().getGroup(groupName); if (RosterManager.isSharedGroup(group)) { it.remove(); } } catch (GroupNotFoundException e) { // Check now if there is a group whose display name matches the requested group Collection<Group> groupsWithProp = GroupManager .getInstance() .search("sharedRoster.displayName", groupName); Iterator<Group> itr = groupsWithProp.iterator(); while(itr.hasNext()) { Group group = itr.next(); if (RosterManager.isSharedGroup(group)) { it.remove(); } } } } this.groups = groups; } } /** * Returns the shared groups for the item. * * @return The shared groups this item belongs to. */ public Collection<Group> getSharedGroups() { Collection<Group> groups = new ArrayList<>(sharedGroups.size()); for (String groupName : sharedGroups) { try { groups.add(GroupManager.getInstance().getGroup(groupName)); } catch (GroupNotFoundException e) { // Do nothing } } return groups; } /** * Returns the invisible shared groups for the item. These groups are for internal use * and help track the reason why a roster item has a presence subscription of type FROM * when using shared groups. * * @return The shared groups this item belongs to. */ public Collection<Group> getInvisibleSharedGroups() { Collection<Group> groups = new ArrayList<>(invisibleSharedGroups.size()); for (String groupName : invisibleSharedGroups) { try { groups.add(GroupManager.getInstance().getGroup(groupName)); } catch (GroupNotFoundException e) { // Do nothing } } return groups; } Set<String> getInvisibleSharedGroupsNames() { return invisibleSharedGroups; } void setInvisibleSharedGroupsNames(Set<String> groupsNames) { invisibleSharedGroups = groupsNames; } /** * Adds a new group to the shared groups list. * * @param sharedGroup The shared group to add to the list of shared groups. */ public void addSharedGroup(Group sharedGroup) { sharedGroups.add(sharedGroup.getName()); invisibleSharedGroups.remove(sharedGroup.getName()); } /** * Adds a new group to the list shared groups that won't be sent to the user. These groups * are for internal use and help track the reason why a roster item has a presence * subscription of type FROM when using shared groups. * * @param sharedGroup The shared group to add to the list of shared groups. */ public void addInvisibleSharedGroup(Group sharedGroup) { invisibleSharedGroups.add(sharedGroup.getName()); } /** * Removes a group from the shared groups list. * * @param sharedGroup The shared group to remove from the list of shared groups. */ public void removeSharedGroup(Group sharedGroup) { sharedGroups.remove(sharedGroup.getName()); invisibleSharedGroups.remove(sharedGroup.getName()); } /** * Returns true if this item belongs to a shared group. Return true even if the item belongs * to a personal group and a shared group. * * @return true if this item belongs to a shared group. */ public boolean isShared() { return !sharedGroups.isEmpty() || !invisibleSharedGroups.isEmpty(); } /** * Returns true if this item belongs ONLY to shared groups. This means that the the item is * considered to be "only shared" if it doesn't belong to a personal group but only to shared * groups. * * @return true if this item belongs ONLY to shared groups. */ public boolean isOnlyShared() { return isShared() && groups.isEmpty(); } /** * Returns the roster ID associated with this particular roster item. A value of zero * means that the roster item is not being persisted in the backend store.<p> * * Databases can use the roster ID as the key in locating roster items. * * @return The roster ID */ public long getID() { return rosterID; } /** * Sets the roster ID associated with this particular roster item. A value of zero * means that the roster item is not being persisted in the backend store.<p> * * Databases can use the roster ID as the key in locating roster items. * * @param rosterID The roster ID. */ public void setID(long rosterID) { this.rosterID = rosterID; } /** * <p>Update the cached item as a copy of the given item.</p> * <p>A convenience for getting the item and setting each attribute.</p> * * @param item The item who's settings will be copied into the cached copy * @throws org.jivesoftware.openfire.SharedGroupException if trying to remove shared group. */ public void setAsCopyOf(org.xmpp.packet.Roster.Item item) throws SharedGroupException { setGroups(new LinkedList<>(item.getGroups())); setNickname(item.getName()); } /* * (non-Javadoc) * * @see org.jivesoftware.util.cache.Cacheable#getCachedSize() */ @Override public int getCachedSize() throws CannotCalculateSizeException { int size = jid.toBareJID().length(); size += CacheSizes.sizeOfString(nickname); size += CacheSizes.sizeOfCollection(groups); size += CacheSizes.sizeOfCollection(invisibleSharedGroups); size += CacheSizes.sizeOfCollection(sharedGroups); size += CacheSizes.sizeOfInt(); // subStatus size += CacheSizes.sizeOfInt(); // askStatus size += CacheSizes.sizeOfInt(); // recvStatus size += CacheSizes.sizeOfLong(); // id return size; } @Override public void writeExternal(ObjectOutput out) throws IOException { ExternalizableUtil.getInstance().writeSerializable(out, jid); ExternalizableUtil.getInstance().writeBoolean(out, nickname != null); if (nickname != null) { ExternalizableUtil.getInstance().writeSafeUTF(out, nickname); } ExternalizableUtil.getInstance().writeStrings(out, groups); ExternalizableUtil.getInstance().writeStrings(out, sharedGroups); ExternalizableUtil.getInstance().writeStrings(out, invisibleSharedGroups); ExternalizableUtil.getInstance().writeInt(out, recvStatus.getValue()); ExternalizableUtil.getInstance().writeInt(out, subStatus.getValue()); ExternalizableUtil.getInstance().writeInt(out, askStatus.getValue()); ExternalizableUtil.getInstance().writeLong(out, rosterID); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { jid = (JID) ExternalizableUtil.getInstance().readSerializable(in); if (ExternalizableUtil.getInstance().readBoolean(in)) { nickname = ExternalizableUtil.getInstance().readSafeUTF(in); } this.groups = new LinkedList<>(); ExternalizableUtil.getInstance().readStrings(in, groups); ExternalizableUtil.getInstance().readStrings(in, sharedGroups); ExternalizableUtil.getInstance().readStrings(in, invisibleSharedGroups); recvStatus = RecvType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in)); subStatus = SubType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in)); askStatus = AskType.getTypeFromInt(ExternalizableUtil.getInstance().readInt(in)); rosterID = ExternalizableUtil.getInstance().readLong(in); } }