/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * 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 net.java.sip.communicator.impl.protocol.icq; import java.util.*; import net.java.sip.communicator.service.protocol.*; import net.kano.joustsim.oscar.oscar.service.ssi.*; /** * The ICQ implementation of the ContactGroup interface. Instances of this class * (contrary to <tt>RootContactGroupIcqImpl</tt>) may only contain buddies * and cannot have sub groups. Note that instances of this class only use the * corresponding joust sim source group for reading their names and only * initially fill their <tt>buddies</tt> <tt>java.util.List</tt> with * the ContactIcqImpl objects corresponding to those contained in the source * group at the moment it is being created. They would, however, never try to * sync or update their contents ulteriorly. This would have to be done through * the addContact()/removeContact() methods. * * @author Emil Ivov */ public class ContactGroupIcqImpl extends AbstractContactGroupIcqImpl { /** * Maps all UINs/Screenname-s to the actual contacts so that we could easily * search the set of existing contacts. Note that we only store lowercase * strings in the left column because screen names in AIM/ICQ are not case * sensitive. */ private final Map<String, Contact> buddies = new Hashtable<String, Contact>(); private boolean isResolved = false; /** * The JoustSIM Group corresponding to this contact group. */ private MutableGroup joustSimSourceGroup = null; /** * a list that would always remain empty. We only use it so that we're able * to extract empty iterators */ private LinkedHashSet<ContactGroup> dummyGroupsList = new LinkedHashSet<ContactGroup>(); /** * A variable that we use as a means of detecting changes in the name * of this group. */ private String nameCopy = null; private ServerStoredContactListIcqImpl ssclCallback = null; /** * Whether current group is persistent. */ private boolean isPersistent; /** * Creates an ICQ group using the specified <tt>joustSimGroup</tt> as * a source. The newly created group will always return the name of the * underlying joustSimGroup and would thus automatically adapt to changes. * It would, however, not receive or try to poll for modifications of the * buddies it contains and would therefore have to be updated manually by * ServerStoredContactListImpl. * <p> * Note that we MUST NOT use the list of buddies obtained through the * getBuddiesCopy() of the joustSimGroup arg as we'd later need to be able * to directly compare ( == ) instances of buddies we've stored and others * that are returned by the framework. * <p> * @param joustSimGroup the JoustSIM Group corresponding to the group * @param groupMembers the group members that we should add to the group. * @param ssclCallback a callback to the server stored contact list * we're creating. * @param isResolved a boolean indicating whether or not the group has been * resolved against the server. */ ContactGroupIcqImpl(MutableGroup joustSimGroup, Iterable<? extends Buddy> groupMembers, ServerStoredContactListIcqImpl ssclCallback, boolean isResolved, boolean isPersistent) { this.joustSimSourceGroup = joustSimGroup; this.isResolved = isResolved; this.isPersistent = isPersistent; this.ssclCallback = ssclCallback; //store a copy of the name now so that we can detect changes in the //name of the underlying joustSimSourceGroup initNameCopy(); //do not use the buddies in the joustSimGroup since we want to keep //their real addresses and we can only get a list of copies from the //group itself. if (groupMembers != null) for (Buddy buddy : groupMembers) { /* * Only add the buddy if it doesn't already exist in some other * group this is necessary because AIM would allow having one * and the same buddy in more than one group. */ if(ssclCallback.findContactByJoustSimBuddy(buddy) != null) { continue; } /* * Here we are not checking for AwaitingAuthorization buddies as * we are creating group with list of buddies these checks must * have been made already. */ addContact( new ContactIcqImpl(buddy, ssclCallback, true, true) ); } } /** * Returns the number of <tt>Contact</tt> members of this * <tt>ContactGroup</tt> * * @return an int indicating the number of <tt>Contact</tt>s, * members of this <tt>ContactGroup</tt>. */ public int countContacts() { return buddies.size(); } /** * Returns a reference to the root icq group which in ICQ is the parent of * any other group since the protocol does not support subgroups. * @return a reference to the root icq group. */ public ContactGroup getParentContactGroup() { return ssclCallback.getRootGroup(); } /** * Adds the specified contact to the end of this group. * @param contact the new contact to add to this group */ void addContact(ContactIcqImpl contact) { buddies.put(contact.getUIN().toLowerCase(), contact); } /** * Removes the specified contact from this contact group * @param contact the contact to remove. */ void removeContact(ContactIcqImpl contact) { if (contact == null ) return; buddies.remove(contact.getUIN().toLowerCase()); } /** * Returns an Iterator over all contacts, member of this * <tt>ContactGroup</tt>. * * @return a java.util.Iterator over all contacts inside this * <tt>ContactGroup</tt>. In case the group doesn't contain any * members it will return an empty iterator. */ public Iterator<Contact> contacts() { return buddies.values().iterator(); } /** * Returns the <tt>Contact</tt> with the specified address or * identifier. * @param id the address or identifier of the <tt>Contact</tt> we are * looking for. * @return the <tt>Contact</tt> with the specified id or address. */ public Contact getContact(String id) { return this.findContact(id); } /** * Returns the name of this group. * @return a String containing the name of this group. */ public String getGroupName() { return joustSimSourceGroup.getName(); } /** * Determines whether the group may contain subgroups or not. * * @return always false since only the root group may contain subgroups. */ public boolean canContainSubgroups() { return false; } /** * Returns the subgroup with the specified index (i.e. always null since * this group may not contain subgroups). * * @param index the index of the <tt>ContactGroup</tt> to retrieve. * @return always null */ public ContactGroup getGroup(int index) { return null; } /** * Returns the subgroup with the specified name. * @param groupName the name of the <tt>ContactGroup</tt> to retrieve. * @return the <tt>ContactGroup</tt> with the specified index. */ public ContactGroup getGroup(String groupName) { return null; } /** * Returns an empty iterator. Subgroups may only be present in the root * group. * * @return an empty iterator */ public Iterator<ContactGroup> subgroups() { return dummyGroupsList.iterator(); } /** * Returns the number of subgroups contained by this group, which is * always 0 since sub groups in the icq protocol may only be contained * by the root group - <tt>RootContactGroupIcqImpl</tt>. * @return a 0 int. */ public int countSubgroups() { return 0; } /** * Returns a hash code value for the object, which is actually the hashcode * value of the groupname. * * @return a hash code value for this ContactGroup. */ @Override public int hashCode() { return getGroupName().hashCode(); } /** * Returns the JoustSIM group that this class is encapsulating. * @return the JoustSIM group corresponding to this SC group. */ MutableGroup getJoustSimSourceGroup() { return joustSimSourceGroup; } /** * Indicates whether some other object is "equal to" this group. A group is * considered equal to another group if it hase the same sets of (equal) * contacts. * <p> * * @param obj the reference object with which to compare. * @return <tt>true</tt> if this object is the same as the obj * argument; <tt>false</tt> otherwise. */ @Override public boolean equals(Object obj) { if( obj == this ) return true; if (obj == null || !(obj instanceof ContactGroupIcqImpl) ) return false; if(!((ContactGroup)obj).getGroupName().equals(getGroupName())) return false; if(getProtocolProvider() != ((ContactGroup)obj).getProtocolProvider()) return false; //since ICQ does not support having two groups with the same name // at this point we could bravely state that the groups are the same // and not bother to compare buddies. (gotta check that though) return true; } /** * Returns the protocol provider that this group belongs to. * @return a regerence to the ProtocolProviderService instance that this * ContactGroup belongs to. */ public ProtocolProviderService getProtocolProvider() { return this.ssclCallback.getParentProvider(); } /** * Returns a string representation of this group, in the form * IcqGroup.GroupName[size]{ buddy1.toString(), buddy2.toString(), ...}. * @return a String representation of the object. */ @Override public String toString() { StringBuffer buff = new StringBuffer("IcqGroup."); buff.append(getGroupName()); buff.append(", childContacts="+countContacts()+":["); Iterator<Contact> contacts = contacts(); while (contacts.hasNext()) { Contact contact = contacts.next(); buff.append(contact.toString()); if(contacts.hasNext()) buff.append(", "); } return buff.append("]").toString(); } /** * Returns the icq contact encapsulating the specified joustSim buddy or null * if no such buddy was found. * * @param joustSimBuddy the buddy whose encapsulating contact we're looking * for. * @return the <tt>ContactIcqImpl</tt> corresponding to the specified * joustSimBuddy or null if no such contact was found. */ ContactIcqImpl findContact(Buddy joustSimBuddy) { Iterator<Contact> contacts = contacts(); while (contacts.hasNext()) { ContactIcqImpl item = (ContactIcqImpl) contacts.next(); if(item.getJoustSimBuddy() == joustSimBuddy) return item; } return null; } /** * Returns the index of contact in this group -1 if no such contact was * found. * * @param contact the contact whose index we're looking for. * @return the index of contact in this group. */ int findContactIndex(Contact contact) { Iterator<Contact> contacts = contacts(); int i = 0; while (contacts.hasNext()) { ContactIcqImpl item = (ContactIcqImpl) contacts.next(); if(item == contact) return i; i++; } return -1; } /** * Returns the icq contact encapsulating with the specified screen name or * null if no such contact was found. * * @param screenName the screenName (or icq UIN) for the contact we're * looking for. * @return the <tt>ContactIcqImpl</tt> corresponding to the specified * screnname or null if no such contact existed. */ ContactIcqImpl findContact(String screenName) { if(screenName == null) return null; String lcScreenName = screenName.toLowerCase(); return (ContactIcqImpl)buddies.get(lcScreenName); } /** * Sets the name copy field that we use as a means of detecting changes in * the group name. */ void initNameCopy() { this.nameCopy = getGroupName(); } /** * Returns the name of the group as it was at the last call of initNameCopy. * @return a String containing a copy of the name of this group as it was * last time when we called <tt>initNameCopy</tt>. */ String getNameCopy() { return this.nameCopy; } /** * Determines whether or not this contact group is being stored by the * server. Non persistent contact groups exist for the sole purpose of * containing non persistent contacts. * @return true if the contact group is persistent and false otherwise. */ public boolean isPersistent() { return isPersistent; } /** * Returns null as no persistent data is required and the contact address is * sufficient for restoring the contact. * <p> * @return null as no such data is needed. */ public String getPersistentData() { return null; } /** * Determines whether or not this contact group has been resolved against * the server. Unresolved group are used when initially loading a contact * list that has been stored in a local file until the presence operation * set has managed to retrieve all the contact list from the server and has * properly mapped contacts and groups to their corresponding on-line * buddies. * @return true if the contact has been resolved (mapped against a buddy) * and false otherwise. */ public boolean isResolved() { return isResolved; } /** * Specifies whether or not this contact group is to be considered resolved * against the server. Note that no actions are to be undertaken against * group buddies in this method. * @param resolved true if this group hase been resolved against the server * and false otherwise. */ void setResolved(boolean resolved) { this.isResolved = resolved; this.isPersistent = true; } /** * Sets this group and contacts corresponding to buddies in the * serverBuddies list as resolved. * @param joustSimGroup the joustSimGroup sent by the server that we should * use to replace the volatile group with. * @param serverBuddies a List of joust sim Buddy objects as they were * returned by the server * @param newContacts a list of ContactIcqImpl objects containing contacts * that were present as joust sim buddies in the <tt>serverBuddies</tt> * list but were not present in the group itself. * @param removedContacts contacts assumed deleted because they were in the * local group but were not in the serverBuddies list. */ void updateGroup(MutableGroup joustSimGroup, List<? extends Buddy> serverBuddies, List<Contact> newContacts, List<ContactIcqImpl> removedContacts) { setResolved(true); this.joustSimSourceGroup = joustSimGroup; for (Buddy buddy : serverBuddies) { ContactIcqImpl contact = findContact(buddy.getScreenname().getFormatted()); if(contact == null) { //if the contact was not in the list, create it and mark it as //new contact = new ContactIcqImpl( buddy, this.ssclCallback, true, true); newContacts.add(contact); addContact(contact); } else { //the contact was already in the list. we need to only set it //as resolved. contact.setJoustSimBuddy(buddy); contact.setResolved(true); } } } /** * Returns a <tt>String</tt> that uniquely represents the group. In this we * use the name of the group as an identifier. This may cause problems * though, in case the name is changed by some other application between * consecutive runs of the sip-communicator. * * @return a String representing this group in a unique and persistent * way. */ public String getUID() { return getGroupName(); } }