/*
* 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.contactlist;
import java.util.*;
import net.java.sip.communicator.service.contactlist.*;
import net.java.sip.communicator.service.contactlist.event.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.util.*;
/**
* A default implementation of the <code>MetaContact</code> interface.
*
* @author Emil Ivov
* @author Lubomir Marinov
*/
public class MetaContactImpl
extends DataObject
implements MetaContact
{
/**
* Logger for <tt>MetaContactImpl</tt>.
*/
private static final Logger logger
= Logger.getLogger(MetaContactImpl.class);
/**
* A vector containing all protocol specific contacts merged in this
* MetaContact.
*/
private final List<Contact> protoContacts = new Vector<Contact>();
/**
* The list of capabilities of the meta contact.
*/
private final Map<String, List<Contact>> capabilities
= new HashMap<String, List<Contact>>();
/**
* The number of contacts online in this meta contact.
*/
private int contactsOnline = 0;
/**
* An id uniquely identifying the meta contact in this contact list.
*/
private final String uid;
/**
* Returns a human readable string used by the UI to display the contact.
*/
private String displayName = "";
/**
* The contact that should be chosen by default when communicating with this
* meta contact.
*/
private Contact defaultContact = null;
/**
* A locally cached copy of an avatar that we should return for lazy calls
* to the getAvatarMethod() in order to speed up display.
*/
private byte[] cachedAvatar = null;
/**
* A flag that tells us whether or not we have already tried to restore
* an avatar from cache. We need this to know whether a <tt>null</tt> cached
* avatar implies that there is no locally stored avatar or that we simply
* haven't tried to retrieve it. This should allow us to only interrogate
* the file system if haven't done so before.
*/
private boolean avatarFileCacheAlreadyQueried = false;
/**
* A callback to the meta contact group that is currently our parent. If
* this is an orphan meta contact that has not yet been added or has been
* removed from a group this callback is going to be null.
*/
private MetaContactGroupImpl parentGroup = null;
/**
* Hashtable containing the contact details.
* Name -> Value or Name -> (List of values).
*/
private Map<String, List<String>> details;
/**
* Whether user has renamed this meta contact.
*/
private boolean isDisplayNameUserDefined = false;
/**
* Creates new meta contact with a newly generated meta contact UID.
*/
MetaContactImpl()
{
//create the uid
this.uid = String.valueOf(System.currentTimeMillis())
+ String.valueOf(hashCode());
this.details = null;
}
/**
* Creates a new meta contact with the specified UID. This constructor
* MUST ONLY be used when restoring contacts stored in the contactlist.xml.
*
* @param metaUID the meta uid that this meta contact should have.
* @param details the already stored details for the contact.
*/
MetaContactImpl(String metaUID, Map<String, List<String>> details)
{
this.uid = metaUID;
this.details = details;
}
/**
* Returns the number of protocol specific <tt>Contact</tt>s that this
* <tt>MetaContact</tt> contains.
*
* @return an int indicating the number of protocol specific contacts
* merged in this <tt>MetaContact</tt>
*/
public int getContactCount()
{
return protoContacts.size();
}
/**
* Returns a Contact, encapsulated by this MetaContact and coming from
* the specified ProtocolProviderService.
* <p>
* In order to prevent problems with concurrency, the <tt>Iterator</tt>
* returned by this method is not be over the actual list of contacts but
* over a copy of that list.
*
* @param provider a reference to the <tt>ProtocolProviderService</tt>
* that we'd like to get a <tt>Contact</tt> for.
* @return a <tt>Contact</tt> encapsulated in this <tt>MetaContact</tt>
* and originating from the specified provider.
*/
public Iterator<Contact> getContactsForProvider(
ProtocolProviderService provider)
{
LinkedList<Contact> providerContacts = new LinkedList<Contact>();
for (Contact contact : protoContacts)
{
if(contact.getProtocolProvider() == provider)
providerContacts.add( contact );
}
return providerContacts.iterator();
}
/**
* Returns all protocol specific Contacts, encapsulated by this MetaContact
* and supporting the given <tt>opSetClass</tt>. If none of the
* contacts encapsulated by this MetaContact is supporting the specified
* <tt>OperationSet</tt> class then an empty iterator is returned.
* <p>
* @param opSetClass the operation for which the default contact is needed
* @return a <tt>List</tt> over all contacts encapsulated in this
* <tt>MetaContact</tt> and supporting the specified <tt>OperationSet</tt>
*/
public List<Contact> getContactsForOperationSet(
Class<? extends OperationSet> opSetClass)
{
LinkedList<Contact> opSetContacts = new LinkedList<Contact>();
for (Contact contact : protoContacts)
{
ProtocolProviderService contactProvider
= contact.getProtocolProvider();
// First try to ask the capabilities operation set if such is
// available.
OperationSetContactCapabilities capOpSet
= contactProvider.getOperationSet(
OperationSetContactCapabilities.class);
if (capOpSet != null)
{
List<Contact> capContacts
= capabilities.get(opSetClass.getName());
if ((capContacts != null) && capContacts.contains(contact))
opSetContacts.add(contact);
}
else if (contactProvider.getOperationSet(opSetClass) != null)
opSetContacts.add(contact);
}
return opSetContacts;
}
/**
* Returns contacts, encapsulated by this MetaContact and belonging to
* the specified protocol ContactGroup.
* <p>
* In order to prevent problems with concurrency, the <tt>Iterator</tt>
* returned by this method is not be over the actual list of contacts but
* over a copy of that list.
*
* @param parentProtoGroup a reference to the <tt>ContactGroup</tt>
* whose children we'd like removed..
* @return an Iterator over all <tt>Contact</tt>s encapsulated in this
* <tt>MetaContact</tt> and belonging to the specified proto ContactGroup.
*/
public Iterator<Contact> getContactsForContactGroup(
ContactGroup parentProtoGroup)
{
List<Contact> providerContacts = new LinkedList<Contact>();
for (Contact contact : protoContacts)
{
if(contact.getParentContactGroup() == parentProtoGroup)
providerContacts.add( contact );
}
return providerContacts.iterator();
}
/**
* Returns a contact encapsulated by this meta contact, having the specified
* contactAddress and coming from the indicated ownerProvider.
* <p>
* @param contactAddress the address of the contact who we're looking for.
* @param ownerProvider a reference to the ProtocolProviderService that
* the contact we're looking for belongs to.
* @return a reference to a <tt>Contact</tt>, encapsulated by this
* MetaContact, carrying the specified address and originating from the
* specified ownerProvider or null if no such contact exists..
*/
public Contact getContact(String contactAddress,
ProtocolProviderService ownerProvider)
{
for (Contact contact : protoContacts)
{
if(contact.getProtocolProvider() == ownerProvider
&& (contact.getAddress().equals(contactAddress)
|| contact.equals(contactAddress)))
return contact;
}
return null;
}
/**
* Returns a contact encapsulated by this meta contact, having the specified
* contactAddress and coming from a provider with a mathing
* <tt>accountID</tt>. The method returns null if no such contact exists.
* <p>
* @param contactAddress the address of the contact who we're looking for.
* @param accountID the identifier of the provider that the contact we're
* looking for must belong to.
* @return a reference to a <tt>Contact</tt>, encapsulated by this
* MetaContact, carrying the specified address and originating from the
* ownerProvider carryign <tt>accountID</tt>.
*/
public Contact getContact(String contactAddress,
String accountID)
{
for (Contact contact : protoContacts)
{
if( contact.getProtocolProvider().getAccountID()
.getAccountUniqueID().equals(accountID)
&& contact.getAddress().equals(contactAddress))
return contact;
}
return null;
}
/**
* Returns <tt>true</tt> if the given <tt>protocolContact</tt> is contained
* in this <tt>MetaContact</tt>, otherwise - returns <tt>false</tt>.
* @param protocolContact the <tt>Contact</tt> we're looking for
* @return <tt>true</tt> if the given <tt>protocolContact</tt> is contained
* in this <tt>MetaContact</tt>, otherwise - returns <tt>false</tt>
*/
public boolean containsContact(Contact protocolContact)
{
return protoContacts.contains(protocolContact);
}
/**
* Returns a <tt>java.util.Iterator</tt> over all protocol specific
* <tt>Contacts</tt> encapsulated by this <tt>MetaContact</tt>.
* <p>
* In order to prevent problems with concurrency, the <tt>Iterator</tt>
* returned by this method is not over the actual list of contacts but over
* a copy of that list.
* <p>
* @return a <tt>java.util.Iterator</tt> over all protocol specific
* <tt>Contact</tt>s that were registered as subcontacts for this
* <tt>MetaContact</tt>
*/
public Iterator<Contact> getContacts()
{
return new LinkedList<Contact>(protoContacts).iterator();
}
/**
* Currently simply returns the most connected protocol contact. We should
* add the possibility to choose it also according to preconfigured
* preferences.
*
* @return the default <tt>Contact</tt> to use when communicating with
* this <tt>MetaContact</tt>
*/
public Contact getDefaultContact()
{
if(defaultContact == null)
{
PresenceStatus currentStatus = null;
for (Contact protoContact : protoContacts)
{
PresenceStatus contactStatus = protoContact.getPresenceStatus();
if (currentStatus != null)
{
if (currentStatus.getStatus() < contactStatus.getStatus())
{
currentStatus = contactStatus;
defaultContact = protoContact;
}
}
else
{
currentStatus = contactStatus;
defaultContact = protoContact;
}
}
}
return defaultContact;
}
/**
* Returns a default contact for a specific operation (call,
* file transfer, IM ...)
*
* @param operationSet the operation for which the default contact is needed
* @return the default contact for the specified operation.
*/
public Contact getDefaultContact(Class<? extends OperationSet> operationSet)
{
Contact defaultOpSetContact = null;
Contact defaultContact = getDefaultContact();
// if the current default contact supports the requested operationSet
// we use it
if (defaultContact != null)
{
ProtocolProviderService contactProvider
= defaultContact.getProtocolProvider();
// First try to ask the capabilities operation set if such is
// available.
OperationSetContactCapabilities capOpSet = contactProvider
.getOperationSet(OperationSetContactCapabilities.class);
if (capOpSet != null)
{
List<Contact> capContacts
= capabilities.get(operationSet.getName());
if (capContacts != null && capContacts.contains(defaultContact))
{
defaultOpSetContact = defaultContact;
}
}
else if (contactProvider.getOperationSet(operationSet) != null)
defaultOpSetContact = defaultContact;
}
if (defaultOpSetContact == null)
{
PresenceStatus currentStatus = null;
for (Contact protoContact : protoContacts)
{
ProtocolProviderService contactProvider
= protoContact.getProtocolProvider();
// First try to ask the capabilities operation set if such is
// available.
OperationSetContactCapabilities capOpSet = contactProvider
.getOperationSet(OperationSetContactCapabilities.class);
// We filter to care only about contact which support
// the needed opset.
if (capOpSet != null)
{
List<Contact> capContacts
= capabilities.get(operationSet.getName());
if (capContacts == null
|| !capContacts.contains(protoContact))
{
continue;
}
}
else if (contactProvider.getOperationSet(operationSet) == null)
continue;
PresenceStatus contactStatus
= protoContact.getPresenceStatus();
if (currentStatus != null)
{
if (currentStatus.getStatus()
< contactStatus.getStatus())
{
currentStatus = contactStatus;
defaultOpSetContact = protoContact;
}
}
else
{
currentStatus = contactStatus;
defaultOpSetContact = protoContact;
}
}
}
return defaultOpSetContact;
}
/**
* Returns a String identifier (the actual contents is left to
* implementations) that uniquely represents this <tt>MetaContact</tt> in
* the containing <tt>MetaContactList</tt>
*
* @return a String uniquely identifying this meta contact.
*/
public String getMetaUID()
{
return uid;
}
/**
* Compares this meta contact with the specified object for order. Returns
* a negative integer, zero, or a positive integer as this meta contact is
* less than, equal to, or greater than the specified object.
* <p>
* The result of this method is calculated the following way:
* <p>
* (contactsOnline - o.contactsOnline) * 1 000 000 <br>
* + getDisplayName().compareTo(o.getDisplayName()) * 100 000
* + getMetaUID().compareTo(o.getMetaUID())<br>
* <p>
* Or in other words ordering of meta accounts would be first done by
* presence status, then display name, and finally (in order to avoid
* equalities) be the fairly random meta contact metaUID.
* <p>
* @param o the <code>MetaContact</code> to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @throws ClassCastException if the specified object is not
* a MetaContactListImpl
*/
public int compareTo(MetaContact o)
{
MetaContactImpl target = (MetaContactImpl) o;
int isOnline
= (contactsOnline > 0)
? 1
: 0;
int targetIsOnline
= (target.contactsOnline > 0)
? 1
: 0;
return ( (10 - isOnline) - (10 - targetIsOnline)) * 100000000
+ getDisplayName().compareToIgnoreCase(target.getDisplayName())
* 10000
+ getMetaUID().compareTo(target.getMetaUID());
}
/**
* Returns a string representation of this contact, containing most of its
* representative details.
*
* @return a string representation of this contact.
*/
@Override
public String toString()
{
StringBuffer buff = new StringBuffer("MetaContact[ DisplayName=")
.append(getDisplayName()).append("]");
return buff.toString();
}
/**
* Returns a characteristic display name that can be used when including
* this <tt>MetaContact</tt> in user interface.
* @return a human readable String that represents this meta contact.
*/
public String getDisplayName()
{
return displayName;
}
/**
* Determines if display name was changed for
* this <tt>MetaContact</tt> in user interface.
* @return whether display name was changed by user.
*/
boolean isDisplayNameUserDefined()
{
return isDisplayNameUserDefined;
}
/**
* Changes that display name was changed for
* this <tt>MetaContact</tt> in user interface.
* @param value control whether display name is user defined
*/
void setDisplayNameUserDefined(boolean value)
{
this.isDisplayNameUserDefined = value;
}
/**
* Queries a specific protocol <tt>Contact</tt> for its avatar. Beware that
* this method could cause multiple network operations. Use with caution.
*
* @param contact the protocol <tt>Contact</tt> to query for its avatar
* @return an array of <tt>byte</tt>s representing the avatar returned by
* the specified <tt>Contact</tt> or <tt>null</tt> if the specified
* <tt>Contact</tt> did not or failed to return an avatar
*/
private byte[] queryProtoContactAvatar(Contact contact)
{
try
{
byte[] contactImage = contact.getImage();
if ((contactImage != null) && (contactImage.length > 0))
return contactImage;
}
catch (Exception ex)
{
logger.error("Failed to get the photo of contact " + contact, ex);
}
return null;
}
/**
* Returns the avatar of this contact, that can be used when including this
* <tt>MetaContact</tt> in user interface. The isLazy parameter would tell
* the implementation if it could return the locally stored avatar or it
* should obtain the avatar right from the server.
*
* @param isLazy Indicates if this method should return the locally stored
* avatar or it should obtain the avatar right from the server.
* @return an avatar (e.g. user photo) of this contact.
*/
public byte[] getAvatar(boolean isLazy)
{
byte[] result = null;
if (!isLazy)
{
// the caller is willing to perform a lengthy operation so let's
// query the proto contacts for their avatars.
Iterator<Contact> protoContacts = getContacts();
while( protoContacts.hasNext())
{
Contact contact = protoContacts.next();
result = queryProtoContactAvatar(contact);
// if we got a result from the above, then let's cache and
// return it.
if ((result != null) && (result.length > 0))
{
cacheAvatar(contact, result);
return result;
}
}
}
//if we get here then the caller is probably not willing to perform
//network operations and opted for a lazy retrieve (... or the
//queryAvatar method returned null because we are calling it too often)
if((cachedAvatar != null) && (cachedAvatar.length > 0))
{
//we already have a cached avatar, so let's return it
return cachedAvatar;
}
//no cached avatar. let's try the file system for previously stored
//ones. (unless we already did this)
if ( avatarFileCacheAlreadyQueried )
return null;
avatarFileCacheAlreadyQueried = true;
Iterator<Contact> iter = this.getContacts();
while (iter.hasNext())
{
Contact protoContact = iter.next();
cachedAvatar = AvatarCacheUtils.getCachedAvatar(protoContact);
/*
* Caching a zero-length avatar happens but such an avatar isn't
* very useful.
*/
if ((cachedAvatar != null) && (cachedAvatar.length > 0))
return cachedAvatar;
}
return null;
}
/**
* Returns an avatar that can be used when presenting this
* <tt>MetaContact</tt> in user interface. The method would also make sure
* that we try the network for new versions of avatars.
*
* @return an avatar (e.g. user photo) of this contact.
*/
public byte[] getAvatar()
{
return getAvatar(false);
}
/**
* Sets a name that can be used when displaying this contact in user
* interface components.
* @param displayName a human readable String representing this
* <tt>MetaContact</tt>
*/
void setDisplayName(String displayName)
{
synchronized (getParentGroupModLock())
{
if (parentGroup != null)
parentGroup.lightRemoveMetaContact(this);
this.displayName = (displayName == null) ? "" : displayName;
if (parentGroup != null)
parentGroup.lightAddMetaContact(this);
}
}
/**
* Adds the specified protocol specific contact to the list of contacts
* merged in this meta contact. The method also keeps up to date the
* contactsOnline field which is used in the compareTo() method.
*
* @param contact the protocol specific Contact to add.
*/
void addProtoContact(Contact contact)
{
synchronized (getParentGroupModLock())
{
if (parentGroup != null)
parentGroup.lightRemoveMetaContact(this);
contactsOnline += contact.getPresenceStatus().isOnline() ? 1 : 0;
this.protoContacts.add(contact);
// Re-init the default contact.
defaultContact = null;
// if this is our first contact and we don't already have a display
// name, use theirs.
if (this.protoContacts.size() == 1
&& (this.displayName == null || this.displayName.trim()
.length() == 0))
{
// be careful not to use setDisplayName() here cause this will
// bring us into a deadlock.
this.displayName = contact.getDisplayName();
}
if (parentGroup != null)
parentGroup.lightAddMetaContact(this);
ProtocolProviderService contactProvider
= contact.getProtocolProvider();
// Check if the capabilities operation set is available for this
// contact and add a listener to it in order to track capabilities'
// changes for all contained protocol contacts.
OperationSetContactCapabilities capOpSet
= contactProvider
.getOperationSet(OperationSetContactCapabilities.class);
if (capOpSet != null)
{
addCapabilities(contact,
capOpSet.getSupportedOperationSets(contact));
}
}
}
/**
* Called by MetaContactListServiceImpl after a contact has changed its
* status, so that ordering in the parent group is updated. The method also
* elects the most connected contact as default contact.
*
* @return the new index at which the contact was added.
*/
int reevalContact()
{
synchronized (getParentGroupModLock())
{
//first lightremove or otherwise we won't be able to get hold of the
//contact
if (parentGroup != null)
{
parentGroup.lightRemoveMetaContact(this);
}
this.contactsOnline = 0;
int maxContactStatus = 0;
for (Contact contact : protoContacts)
{
int contactStatus = contact.getPresenceStatus()
.getStatus();
if(maxContactStatus < contactStatus)
{
maxContactStatus = contactStatus;
this.defaultContact = contact;
}
if (contact.getPresenceStatus().isOnline())
contactsOnline++;
}
//now read it and the contact would be automatically placed
//properly by the containing group
if (parentGroup != null)
{
return parentGroup.lightAddMetaContact(this);
}
}
return -1;
}
/**
* Removes the specified protocol specific contact from the contacts
* encapsulated in this <code>MetaContact</code>. The method also updates
* the total status field accordingly. And updates its ordered position
* in its parent group. If the display name of this <code>MetaContact</code>
* was the one of the removed contact, we update it.
*
* @param contact the contact to remove
*/
void removeProtoContact(Contact contact)
{
synchronized (getParentGroupModLock())
{
if (parentGroup != null)
parentGroup.lightRemoveMetaContact(this);
contactsOnline -= contact.getPresenceStatus().isOnline() ? 1 : 0;
this.protoContacts.remove(contact);
if (defaultContact == contact)
defaultContact = null;
if ((protoContacts.size() > 0)
&& displayName.equals(contact.getDisplayName()))
{
displayName = getDefaultContact().getDisplayName();
}
if (parentGroup != null)
parentGroup.lightAddMetaContact(this);
ProtocolProviderService contactProvider
= contact.getProtocolProvider();
// Check if the capabilities operation set is available for this
// contact and add a listener to it in order to track capabilities'
// changes for all contained protocol contacts.
OperationSetContactCapabilities capOpSet
= contactProvider
.getOperationSet(OperationSetContactCapabilities.class);
if (capOpSet != null)
{
removeCapabilities(contact,
capOpSet.getSupportedOperationSets(contact));
}
}
}
/**
* Removes all proto contacts that belong to the specified provider.
*
* @param provider the provider whose contacts we want removed.
*
* @return true if this <tt>MetaContact</tt> was modified and false
* otherwise.
*/
boolean removeContactsForProvider(ProtocolProviderService provider)
{
boolean modified = false;
Iterator<Contact> contactsIter = protoContacts.iterator();
while(contactsIter.hasNext())
{
Contact contact = contactsIter.next();
if (contact.getProtocolProvider() == provider)
{
contactsIter.remove();
modified = true;
}
}
// if the default contact has been modified, set it to null
if (modified && !protoContacts.contains(defaultContact))
{
defaultContact = null;
}
return modified;
}
/**
* Removes all proto contacts that belong to the specified protocol group.
*
* @param protoGroup the group whose children we want removed.
*
* @return true if this <tt>MetaContact</tt> was modified and false
* otherwise.
*/
boolean removeContactsForGroup(ContactGroup protoGroup)
{
boolean modified = false;
Iterator<Contact> contactsIter = protoContacts.iterator();
while(contactsIter.hasNext())
{
Contact contact = contactsIter.next();
if (contact.getParentContactGroup() == protoGroup)
{
contactsIter.remove();
modified = true;
}
}
// if the default contact has been modified, set it to null
if (modified && !protoContacts.contains(defaultContact))
{
defaultContact = null;
}
return modified;
}
/**
* Sets <tt>parentGroup</tt> as a parent of this meta contact. Do not
* call this method with a null argument even if a group is removing
* this contact from itself as this could lead to race conditions (imagine
* another group setting itself as the new parent and you removing it).
* Use unsetParentGroup instead.
*
* @param parentGroup the <tt>MetaContactGroupImpl</tt> that is currently a
* parent of this meta contact.
* @throws NullPointerException if <tt>parentGroup</tt> is null.
*/
void setParentGroup(MetaContactGroupImpl parentGroup)
{
if (parentGroup == null)
throw new NullPointerException("Do not call this method with a "
+ "null argument even if a group is removing this contact "
+ "from itself as this could lead to race conditions "
+ "(imagine another group setting itself as the new "
+ "parent and you removing it). Use unsetParentGroup "
+ "instead.");
synchronized (getParentGroupModLock())
{
this.parentGroup = parentGroup;
}
}
/**
* If <tt>parentGroup</tt> was the parent of this meta contact then it
* sets it to null. Call this method when removing this contact from a
* meta contact group.
* @param parentGrp the <tt>MetaContactGroupImpl</tt> that we don't want
* considered as a parent of this contact any more.
*/
void unsetParentGroup(MetaContactGroupImpl parentGrp)
{
synchronized(getParentGroupModLock())
{
if (parentGroup == parentGrp)
parentGroup = null;
}
}
/**
* Returns the group that is currently holding this meta contact.
*
* @return the group that is currently holding this meta contact.
*/
MetaContactGroupImpl getParentGroup()
{
return parentGroup;
}
/**
* Returns the MetaContactGroup currently containing this meta contact
* @return a reference to the MetaContactGroup currently containing this
* meta contact.
*/
public MetaContactGroup getParentMetaContactGroup()
{
return getParentGroup();
}
/**
* Adds a custom detail to this contact.
* @param name name of the detail.
* @param value the value of the detail.
*/
public void addDetail(String name, String value)
{
if (details == null)
details = new Hashtable<String, List<String>>();
List<String> values = details.get(name);
if (values == null)
{
values = new ArrayList<String>();
details.put(name, values);
}
values.add(value);
fireMetaContactModified(name, null, value);
}
/**
* Remove the given detail.
* @param name of the detail to be removed.
* @param value value of the detail to be removed.
*/
public void removeDetail(String name, String value)
{
if (details == null)
return;
List<String> values = details.get(name);
if (values == null)
return;
values.remove(value);
fireMetaContactModified(name, value, null);
}
/**
* Remove all details with given name.
* @param name of the details to be removed.
*/
public void removeDetails(String name)
{
if (details == null)
return;
Object removed = details.remove(name);
fireMetaContactModified(name, removed, null);
}
/**
* Change the detail.
* @param name of the detail to be changed.
* @param oldValue the old value of the detail.
* @param newValue the new value of the detail.
*/
public void changeDetail(String name, String oldValue, String newValue)
{
if (details == null)
return;
List<String> values = details.get(name);
if(values == null)
return;
int changedIx = values.indexOf(oldValue);
if(changedIx == -1)
return;
values.set(changedIx, newValue);
fireMetaContactModified(name, oldValue, newValue);
}
/**
* Fires a new <tt>MetaContactModifiedEvent</tt> which is to notify about a
* modification with a specific name of this <tt>MetaContact</tt> which has
* caused a property value change from a specific <tt>oldValue</tt> to a
* specific <tt>newValue</tt>.
*
* @param modificationName the name of the modification which has caused
* a new <tt>MetaContactModifiedEvent</tt> to be fired
* @param oldValue the value of the property before the modification
* @param newValue the value of the property after the modification
*/
private void fireMetaContactModified(
String modificationName,
Object oldValue,
Object newValue)
{
MetaContactGroupImpl parentGroup = getParentGroup();
if (parentGroup != null)
parentGroup
.getMclServiceImpl()
.fireMetaContactEvent(
new MetaContactModifiedEvent(
this,
modificationName,
oldValue,
newValue));
}
/**
* Gets all details with a given name.
*
* @param name the name of the details we are searching for
* @return a <tt>List</tt> of <tt>String</tt>s which represent the details
* with the specified <tt>name</tt>
*/
public List<String> getDetails(String name)
{
List<String> values = (details == null) ? null : details.get(name);
if(values == null)
values = new ArrayList<String>();
else
values = new ArrayList<String>(values);
return values;
}
/**
* Stores avatar bytes in the given <tt>Contact</tt>.
*
* @param protoContact The contact in which we store the avatar.
* @param avatarBytes The avatar image bytes.
*/
public void cacheAvatar( Contact protoContact,
byte[] avatarBytes)
{
this.cachedAvatar = avatarBytes;
this.avatarFileCacheAlreadyQueried = true;
AvatarCacheUtils.cacheAvatar(protoContact, avatarBytes);
}
/**
* Updates the capabilities for the given contact.
*
* @param contact the <tt>Contact</tt>, which capabilities have changed
* @param opSets the new updated set of operation sets
*/
public void updateCapabilities( Contact contact,
Map<String, ? extends OperationSet> opSets)
{
OperationSetContactCapabilities capOpSet
= contact.getProtocolProvider().getOperationSet(
OperationSetContactCapabilities.class);
// This should not happen, because this method is called explicitly for
// events coming from the capabilities operation set.
if (capOpSet == null)
return;
removeCapabilities(contact, opSets);
addCapabilities(contact, opSets);
}
/**
* Remove capabilities for the given contacts.
*
* @param contact the <tt>Contact</tt>, which capabilities we remove
* @param opSets the new updated set of operation sets
*/
private void removeCapabilities(Contact contact,
Map<String, ? extends OperationSet> opSets)
{
Iterator<Map.Entry<String, List<Contact>>> caps
= this.capabilities.entrySet().iterator();
Set<String> contactNewCaps = opSets.keySet();
while (caps.hasNext())
{
Map.Entry<String, List<Contact>> entry = caps.next();
String opSetName = entry.getKey();
List<Contact> contactsForCap = entry.getValue();
if (contactsForCap.contains(contact)
&& !contactNewCaps.contains(opSetName))
{
contactsForCap.remove(contact);
if (contactsForCap.size() == 0)
caps.remove();
}
}
}
/**
* Adds the capabilities of the given contact.
*
* @param contact the <tt>Contact</tt>, which capabilities we add
* @param opSets the map of operation sets supported by the contact
*/
private void addCapabilities( Contact contact,
Map<String, ? extends OperationSet> opSets)
{
Iterator<String> contactNewCaps = opSets.keySet().iterator();
while (contactNewCaps.hasNext())
{
String newCap = contactNewCaps.next();
List<Contact> capContacts = null;
if (!capabilities.containsKey(newCap))
{
capContacts = new LinkedList<Contact>();
capContacts.add(contact);
capabilities.put(newCap, capContacts);
}
else
{
capContacts = capabilities.get(newCap);
if (!capContacts.contains(contact))
{
capContacts.add(contact);
}
}
}
}
/**
* Gets the sync lock for use when modifying {@link #parentGroup}.
*
* @return the sync lock for use when modifying {@link #parentGroup}
*/
private Object getParentGroupModLock()
{
/*
* XXX The use of uid as parentGroupModLock is a bit unusual but a
* dedicated lock enlarges the shallow runtime size of this instance and
* having hundreds of MetaContactImpl instances is not unusual for a
* multi-protocol application. With respect to parentGroupModLock being
* unique among the MetaContactImpl instances, uid is fine because it is
* also supposed to be unique in the same way.
*/
return uid;
}
}