/**
* $RCSfile: ,v $
* $Revision: $
* $Date: $
*
* Copyright (C) 2004-2011 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.sparkimpl.profile;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.PacketInterceptor;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.packet.VCard;
import org.jivesoftware.smackx.provider.VCardProvider;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.ui.ContactItem;
import org.jivesoftware.spark.util.Base64;
import org.jivesoftware.spark.util.GraphicUtils;
import org.jivesoftware.spark.util.ModelUtil;
import org.jivesoftware.spark.util.ResourceUtils;
import org.jivesoftware.spark.util.SwingWorker;
import org.jivesoftware.spark.util.TaskEngine;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.manager.Enterprise;
import org.jivesoftware.sparkimpl.profile.ext.JabberAvatarExtension;
import org.jivesoftware.sparkimpl.profile.ext.VCardUpdateExtension;
import org.xmlpull.mxp1.MXParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
/**
* VCardManager handles all VCard loading/caching within Spark.
*
* @author Derek DeMoro
*/
public class VCardManager {
private VCard personalVCard;
private Map<String, VCard> vcards = Collections.synchronizedMap(new HashMap<String, VCard>());
private Set<String> delayedContacts = Collections.synchronizedSet(new HashSet<String>());
private boolean vcardLoaded;
private File imageFile;
private final VCardEditor editor;
private File vcardStorageDirectory;
final MXParser parser;
private LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>();
private File contactsDir;
private List<VCardListener> listeners = new ArrayList<VCardListener>();
private List<String> writingQueue = Collections.synchronizedList(new ArrayList<String>());
/**
* Initialize VCardManager.
*/
public VCardManager() {
// Initialize parser
parser = new MXParser();
try {
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
}
catch (XmlPullParserException e) {
Log.error(e);
}
imageFile = new File(SparkManager.getUserDirectory(), "personal.png");
// Initialize vCard.
personalVCard = new VCard();
// Set VCard Storage
vcardStorageDirectory = new File(SparkManager.getUserDirectory(), "vcards");
vcardStorageDirectory.mkdirs();
// Set the current user directory.
contactsDir = new File(SparkManager.getUserDirectory(), "contacts");
contactsDir.mkdirs();
initializeUI();
// Intercept all presence packets being sent and append vcard information.
PacketFilter presenceFilter = new PacketTypeFilter(Presence.class);
SparkManager.getConnection().addPacketInterceptor(new PacketInterceptor() {
public void interceptPacket(Packet packet) {
Presence newPresence = (Presence)packet;
VCardUpdateExtension update = new VCardUpdateExtension();
JabberAvatarExtension jax = new JabberAvatarExtension();
PacketExtension updateExt = newPresence.getExtension(update.getElementName(), update.getNamespace());
PacketExtension jabberExt = newPresence.getExtension(jax.getElementName(), jax.getNamespace());
if (updateExt != null) {
newPresence.removeExtension(updateExt);
}
if (jabberExt != null) {
newPresence.removeExtension(jabberExt);
}
if (personalVCard != null) {
byte[] bytes = personalVCard.getAvatar();
if (bytes != null && bytes.length > 0) {
update.setPhotoHash(personalVCard.getAvatarHash());
jax.setPhotoHash(personalVCard.getAvatarHash());
newPresence.addExtension(update);
newPresence.addExtension(jax);
}
}
}
}, presenceFilter);
editor = new VCardEditor();
// Start Listener
startQueueListener();
}
/**
* Listens for new VCards to lookup in a queue.
*/
private void startQueueListener() {
final Runnable queueListener = new Runnable() {
public void run() {
while (true) {
try {
String jid = queue.take();
reloadVCard(jid);
}
catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
TaskEngine.getInstance().submit(queueListener);
PacketFilter filter = new PacketTypeFilter(VCard.class);
PacketListener myListener = new PacketListener() {
@Override
public void processPacket(Packet packet) {
if (packet instanceof VCard)
{
VCard VCardpacket = (VCard)packet;
String jid = VCardpacket.getFrom();
if (VCardpacket.getType().equals(IQ.Type.RESULT) && jid != null && delayedContacts.contains(jid))
{
delayedContacts.remove(jid);
addVCard(jid, VCardpacket);
persistVCard(jid, VCardpacket);
}
}
}
};
SparkManager.getConnection().addPacketListener(myListener, filter);
}
/**
* Adds a jid to lookup vCard.
*
* @param jid the jid to lookup.
*/
public void addToQueue(String jid) {
if (!queue.contains(jid)) {
queue.add(jid);
}
}
/**
* Adds VCard capabilities to menus and other components in Spark.
*/
private void initializeUI() {
boolean enabled = Enterprise.containsFeature(Enterprise.VCARD_FEATURE);
if (!enabled) {
return;
}
// Add Actions Menu
final JMenu contactsMenu = SparkManager.getMainWindow().getMenuByName(Res.getString("menuitem.contacts"));
final JMenu communicatorMenu = SparkManager.getMainWindow().getJMenuBar().getMenu(0);
JMenuItem editProfileMenu = new JMenuItem(SparkRes.getImageIcon(SparkRes.SMALL_BUSINESS_MAN_VIEW));
ResourceUtils.resButton(editProfileMenu, Res.getString("menuitem.edit.my.profile"));
int size = contactsMenu.getMenuComponentCount();
communicatorMenu.insert(editProfileMenu, 1);
editProfileMenu.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
SwingWorker vcardLoaderWorker = new SwingWorker() {
public Object construct() {
try {
personalVCard.load(SparkManager.getConnection());
}
catch (XMPPException e) {
Log.error("Error loading vcard information.", e);
}
return true;
}
public void finished() {
editor.editProfile(personalVCard, SparkManager.getWorkspace());
}
};
vcardLoaderWorker.start();
}
});
JMenuItem viewProfileMenu = new JMenuItem("", SparkRes.getImageIcon(SparkRes.FIND_TEXT_IMAGE));
ResourceUtils.resButton(viewProfileMenu, Res.getString("menuitem.lookup.profile"));
contactsMenu.insert(viewProfileMenu, size > 0 ? size - 1 : 0);
viewProfileMenu.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String jidToView = JOptionPane.showInputDialog(SparkManager.getMainWindow(), Res.getString("message.enter.jabber.id") + ":", Res.getString("title.lookup.profile"), JOptionPane.QUESTION_MESSAGE);
if (ModelUtil.hasLength(jidToView) && jidToView.indexOf("@") != -1 && ModelUtil.hasLength(StringUtils.parseServer(jidToView))) {
viewProfile(jidToView, SparkManager.getWorkspace());
}
else if (ModelUtil.hasLength(jidToView)) {
JOptionPane.showMessageDialog(SparkManager.getMainWindow(), Res.getString("message.invalid.jabber.id"), Res.getString("title.error"), JOptionPane.ERROR_MESSAGE);
}
}
});
}
/**
* Displays <code>VCardViewer</code> for a particular JID.
*
* @param jid the jid of the user to display.
* @param parent the parent component to use for displaying dialog.
*/
public void viewProfile(final String jid, final JComponent parent) {
final SwingWorker vcardThread = new SwingWorker() {
VCard vcard = new VCard();
public Object construct() {
vcard = getVCard(jid);
return vcard;
}
public void finished() {
if (vcard == null) {
// Show vcard not found
JOptionPane.showMessageDialog(parent, Res.getString("message.unable.to.load.profile", jid), Res.getString("title.profile.not.found"), JOptionPane.ERROR_MESSAGE);
}
else {
editor.displayProfile(jid, vcard, parent);
}
}
};
vcardThread.start();
}
/**
* Displays the full profile for a particular JID.
*
* @param jid the jid of the user to display.
* @param parent the parent component to use for displaying dialog.
*/
public void viewFullProfile(final String jid, final JComponent parent) {
final SwingWorker vcardThread = new SwingWorker() {
VCard vcard = new VCard();
public Object construct() {
vcard = getVCard(jid);
return vcard;
}
public void finished() {
if (vcard.getError() != null || vcard == null) {
// Show vcard not found
JOptionPane.showMessageDialog(parent, Res.getString("message.unable.to.load.profile", jid), Res.getString("title.profile.not.found"), JOptionPane.ERROR_MESSAGE);
}
else {
editor.viewFullProfile(vcard, parent);
}
}
};
vcardThread.start();
}
/**
* Returns the VCard for this Spark user. This information will be cached after loading.
*
* @return this users VCard.
*/
public VCard getVCard() {
if (!vcardLoaded) {
reloadPersonalVCard();
vcardLoaded = true;
}
return personalVCard;
}
/**
* Loads the vcard for this Spark user
* @return this users VCard.
*/
public void reloadPersonalVCard() {
try {
personalVCard.load(SparkManager.getConnection());
// If VCard is loaded, then save the avatar to the personal folder.
byte[] bytes = personalVCard.getAvatar();
if (bytes != null && bytes.length > 0) {
ImageIcon icon = new ImageIcon(bytes);
icon = VCardManager.scale(icon);
if (icon != null && icon.getIconWidth() != -1) {
BufferedImage image = GraphicUtils.convert(icon.getImage());
ImageIO.write(image, "PNG", imageFile);
}
}
}
catch (Exception e) {
personalVCard.setError(new XMPPError(XMPPError.Condition.conflict));
Log.error(e);
}
}
/**
* Returns the Avatar in the form of an <code>ImageIcon</code>.
*
* @param vcard the vCard containing the avatar.
* @return the ImageIcon or null if no avatar was present.
*/
public static ImageIcon getAvatarIcon(VCard vcard) {
// Set avatar
byte[] bytes = vcard.getAvatar();
if (bytes != null && bytes.length > 0) {
ImageIcon icon = new ImageIcon(bytes);
return GraphicUtils.scaleImageIcon(icon, 40, 40);
}
return null;
}
/**
* Returns the VCard. Will first look in VCard cache. You will receive a
* dummy vcard, if there is no vCard for specified jid in cache. Same as
* getVCard(jid, true)
*
* @param jid
* the users jid.
* @return the VCard.
*/
public VCard getVCard(String jid) {
return getVCard(jid, true);
}
/**
* Loads the vCard from memory. If no vCard is found in memory, will add it
* to a loading queue for future loading. Users of this method should only
* use it if the correct vCard is not important the first time around. You
* will get a dummy vCard if there is currently no vCard in memory.
*
* @param jid
* the users jid.
* @return the users VCard or an empty VCard.
*/
public VCard getVCardFromMemory(String jid) {
// Check in memory first.
if (vcards.containsKey(jid)) {
return vcards.get(jid);
}
// if not in memory
VCard vcard = loadFromFileSystem(jid);
if (vcard == null) {
addToQueue(jid);
// Create temp vcard.
vcard = new VCard();
vcard.setJabberId(jid);
} else {
//System.out.println(jid+" HDD ---------->");
}
return vcard;
}
/**
* Returns the VCard. You should always use useChachedVCards. VCardManager
* will keep the VCards up to date. If you wan't to force a network reload
* of the VCard you can set useChachedVCards to false. That means that you
* have to wait for the vcard response. The method will block until the
* result is available or a timeout occurs (like reloadVCard(String jid)).
* If there is no response from server this method a dummy vcard with an
* error. Use getVCard(String jid) to get a dummy VCard if there is
* currently no VCard. If you get a vCard with an error you may wait some
* seconds. Sometimes vCards could not be loaded within smack timeout but we
* are still listening for vCards that are too late. Be patient for some
* seconds and try again, maybe we will get it.
*
* @param jid
* the users jid.
* @param useCache
* true to check in cache and hdd, otherwise false will do a new
* network vcard operation.
* @return the VCard.
*/
public VCard getVCard(String jid, boolean useCachedVCards) {
jid = StringUtils.parseBareAddress(jid);
if (useCachedVCards)
{
return getVCardFromMemory(jid);
} else {
return reloadVCard(jid);
}
}
/**
* Forces a reload of a <code>VCard</code>. To load a VCard you should use
* getVCard(String jid) instead. This method will perform a network lookup
* which could take some time. If you're having problems with request
* timeout you should also use getVCard(String jid). Use addToQueue(String
* jid) if you want VCardManager to update the VCard by the given jid. The
* method will block until the result is available or a timeout occurs.
*
* @param jid
* the jid of the user.
*
* @return the new network vCard or a vCard with an error
*/
public VCard reloadVCard(String jid) {
jid = StringUtils.parseBareAddress(jid);
VCard vcard = new VCard();
try {
vcard.setJabberId(jid);
vcard.load(SparkManager.getConnection(), jid);
if (vcard.getNickName() != null && vcard.getNickName().length() > 0)
{
// update nickname.
ContactItem item = SparkManager.getWorkspace().getContactList().getContactItemByJID(jid);
item.setNickname(vcard.getNickName());
// TODO: this doesn't work if someone removes his nickname. If we remove it in that case, it will cause problems with people using another way to manage their nicknames.
}
addVCard(jid, vcard);
persistVCard(jid, vcard);
}
catch (XMPPException e) {
////System.out.println(jid+" Fehler in reloadVCard ----> null");
vcard.setError(new XMPPError(XMPPError.Condition.request_timeout));
vcard.setJabberId(jid);
delayedContacts.add(jid);
return vcard;
//We dont want cards with error
// vcard.setError(new XMPPError(XMPPError.Condition.request_timeout));
//addVCard(jid, vcard);
}
// Persist XML
return vcard;
}
/**
* Adds a new vCard to the cache.
*
* @param jid the jid of the user.
* @param vcard the users vcard to cache.
*/
public void addVCard(String jid, VCard vcard) {
if (vcard == null)
return;
vcard.setJabberId(jid);
if (vcards.containsKey(jid) && vcards.get(jid).getError() == null && vcard.getError()!= null)
{
return;
}
vcards.put(jid, vcard);
}
/**
* Scales an image to the preferred avatar size.
*
* @param icon the icon to scale.
* @return the scaled version of the image.
*/
public static ImageIcon scale(ImageIcon icon) {
Image avatarImage = icon.getImage();
if (icon.getIconHeight() > 64 || icon.getIconWidth() > 64) {
avatarImage = avatarImage.getScaledInstance(-1, 64, Image.SCALE_SMOOTH);
}
return new ImageIcon(avatarImage);
}
/**
* Returns the URL of the avatar image associated with the users JID.
*
* @param jid the jid of the user.
* @return the URL of the image. If not image is found, a default avatar is returned.
*/
public URL getAvatar(String jid) {
// Handle own avatar file.
if (jid != null && StringUtils.parseBareAddress(SparkManager.getSessionManager().getJID()).equals(StringUtils.parseBareAddress(jid))) {
if (imageFile.exists()) {
try {
return imageFile.toURI().toURL();
}
catch (MalformedURLException e) {
Log.error(e);
}
}
else {
return SparkRes.getURL(SparkRes.DUMMY_CONTACT_IMAGE);
}
}
// Handle other users JID
ContactItem item = SparkManager.getWorkspace().getContactList().getContactItemByJID(jid);
URL avatarURL = null;
if (item != null) {
try {
avatarURL = item.getAvatarURL();
}
catch (MalformedURLException e) {
Log.error(e);
}
}
if (avatarURL == null) {
return SparkRes.getURL(SparkRes.DUMMY_CONTACT_IMAGE);
}
return avatarURL;
}
/**
* Searches all vCards for a specified phone number.
*
* @param phoneNumber the phoneNumber.
* @return the vCard which contains the phone number.
*/
public VCard searchPhoneNumber(String phoneNumber) {
for (VCard vcard : vcards.values()) {
String homePhone = getNumbersFromPhone(vcard.getPhoneHome("VOICE"));
String workPhone = getNumbersFromPhone(vcard.getPhoneWork("VOICE"));
String cellPhone = getNumbersFromPhone(vcard.getPhoneWork("CELL"));
String query = getNumbersFromPhone(phoneNumber);
if ((homePhone != null && homePhone.endsWith(query)) ||
(workPhone != null && workPhone.endsWith(query)) ||
(cellPhone != null && cellPhone.endsWith(query))) {
return vcard;
}
}
return null;
}
/**
* Parses out the numbers only from a phone number.
*
* @param number the full phone number.
* @return the phone number only (5551212)
*/
public static String getNumbersFromPhone(String number) {
if (number == null) {
return null;
}
number = number.replace("-", "");
number = number.replace("(", "");
number = number.replace(")", "");
number = number.replace(" ", "");
return number;
}
/**
* Sets the personal vcard of the user.
*
* @param vcard the users vCard.
*/
public void setPersonalVCard(VCard vcard) {
this.personalVCard = vcard;
}
public URL getAvatarURL(String jid) {
VCard vcard = getVCard(jid);
if (vcard != null) {
String hash = vcard.getAvatarHash();
if (!ModelUtil.hasLength(hash)) {
return null;
}
final File avatarFile = new File(contactsDir, hash);
try {
return avatarFile.toURI().toURL();
}
catch (MalformedURLException e) {
Log.error(e);
}
}
return null;
}
/**
* Get URL for avatar from vcard. If there is no vcard available we will try
* to get it from the server and return null.
*
* @param jid
* the users jid
* @return the vcard if there is already one, otherwise null and we try to
* load vcard in background
*
*/
public URL getAvatarURLIfAvailable(String jid) {
if (getVCard(jid) != null) {
return getAvatarURL(jid);
} else {
addToQueue(jid);
return null;
}
}
/**
* Persist vCard information out for caching.
*
* @param jid the users jid.
* @param vcard the users vcard.
*/
private void persistVCard(String jid, VCard vcard) {
if (jid == null || jid.trim().isEmpty() || vcard == null) {
return;
}
String fileName = Base64.encodeBytes(jid.getBytes());
// remove tab
fileName = fileName.replaceAll("\t", "");
// remove new line (Unix)
fileName = fileName.replaceAll("\n", "");
// remove new line (Windows)
fileName = fileName.replaceAll("\r", "");
byte[] bytes = vcard.getAvatar();
if (bytes != null && bytes.length > 0) {
vcard.setAvatar(bytes);
try {
String hash = vcard.getAvatarHash();
final File avatarFile = new File(contactsDir, hash);
ImageIcon icon = new ImageIcon(bytes);
icon = VCardManager.scale(icon);
if (icon != null && icon.getIconWidth() != -1) {
BufferedImage image = GraphicUtils.convert(icon.getImage());
if (image == null) {
Log.warning("Unable to write out avatar for " + jid);
}
else {
if (writingQueue.contains(jid)) {
writeAvatarSync(image, avatarFile);
} else {
writingQueue.add(jid);
ImageIO.write(image, "PNG", avatarFile);
writingQueue.remove(jid);
}
}
}
}
catch (Exception e) {
Log.error("Unable to update avatar in Contact Item.", e);
}
}
// Set timestamp
vcard.setField("timestamp", Long.toString(System.currentTimeMillis()));
final String xml = vcard.toString();
File vcardFile = new File(vcardStorageDirectory, fileName);
// write xml to file
try {
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(vcardFile), "UTF-8"));
out.write(xml);
out.close();
}
catch (IOException e) {
Log.error(e);
}
}
private synchronized void writeAvatarSync(BufferedImage image, File avatarFile) throws IOException {
ImageIO.write(image, "PNG", avatarFile);
}
/**
* Attempts to load
*
* @param jid the jid of the user.
* @return the VCard if found, otherwise null.
*/
private VCard loadFromFileSystem(String jid) {
if (jid == null || jid.trim().isEmpty()) {
return null;
}
// Unescape JID
String fileName = Base64.encodeBytes(jid.getBytes());
// remove tab
fileName = fileName.replaceAll("\t", "");
// remove new line (Unix)
fileName = fileName.replaceAll("\n", "");
// remove new line (Windows)
fileName = fileName.replaceAll("\r", "");
final File vcardFile = new File(vcardStorageDirectory, fileName);
if (!vcardFile.exists()) {
return null;
}
try {
// Otherwise load from file system.
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(vcardFile), "UTF-8"));
VCardProvider provider = new VCardProvider();
parser.setInput(in);
VCard vcard = (VCard)provider.parseIQ(parser);
// Check to see if the file is older 10 minutes. If so, reload.
String timestamp = vcard.getField("timestamp");
if (timestamp != null) {
long time = Long.parseLong(timestamp);
long now = System.currentTimeMillis();
long hour = (1000 * 60) * 60;
if (now - time > hour) {
addToQueue(jid);
}
}
addVCard(jid, vcard);
return vcard;
}
catch (Exception e) {
Log.warning("Unable to load vCard for " + jid, e);
}
return null;
}
/**
* Add <code>VCardListener</code>. Listens to the personalVCard.
*
* @param listener the listener to add.
*/
public void addVCardListener(VCardListener listener) {
listeners.add(listener);
}
/**
* Remove <code>VCardListener</code>.
*
* @param listener the listener to remove.
*/
public void removeVCardListener(VCardListener listener) {
listeners.remove(listener);
}
/**
* Notify all <code>VCardListener</code> implementations.
*/
protected void notifyVCardListeners() {
for (VCardListener listener : listeners) {
listener.vcardChanged(personalVCard);
}
}
}