blob: 9766db82404433c75202023f313343c6e8ca3356 [file] [log] [blame]
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright 2003-2007 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.smackx.packet;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.StringUtils;
/**
* A VCard class for use with the
* <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p>
* <p/>
* You should refer to the
* <a href="http://www.jabber.org/jeps/jep-0054.html" target="_blank">JEP-54 documentation</a>.<p>
* <p/>
* Please note that this class is incomplete but it does provide the most commonly found
* information in vCards. Also remember that VCard transfer is not a standard, and the protocol
* may change or be replaced.<p>
* <p/>
* <b>Usage:</b>
* <pre>
* <p/>
* // To save VCard:
* <p/>
* VCard vCard = new VCard();
* vCard.setFirstName("kir");
* vCard.setLastName("max");
* vCard.setEmailHome("foo@fee.bar");
* vCard.setJabberId("jabber@id.org");
* vCard.setOrganization("Jetbrains, s.r.o");
* vCard.setNickName("KIR");
* <p/>
* vCard.setField("TITLE", "Mr");
* vCard.setAddressFieldHome("STREET", "Some street");
* vCard.setAddressFieldWork("CTRY", "US");
* vCard.setPhoneWork("FAX", "3443233");
* <p/>
* vCard.save(connection);
* <p/>
* // To load VCard:
* <p/>
* VCard vCard = new VCard();
* vCard.load(conn); // load own VCard
* vCard.load(conn, "joe@foo.bar"); // load someone's VCard
* </pre>
*
* @author Kirill Maximov (kir@maxkir.com)
*/
public class VCard extends IQ {
/**
* Phone types:
* VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF?
*/
private Map<String, String> homePhones = new HashMap<String, String>();
private Map<String, String> workPhones = new HashMap<String, String>();
/**
* Address types:
* POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?,
* REGION?, PCODE?, CTRY?
*/
private Map<String, String> homeAddr = new HashMap<String, String>();
private Map<String, String> workAddr = new HashMap<String, String>();
private String firstName;
private String lastName;
private String middleName;
private String emailHome;
private String emailWork;
private String organization;
private String organizationUnit;
private String photoMimeType;
private String photoBinval;
/**
* Such as DESC ROLE GEO etc.. see JEP-0054
*/
private Map<String, String> otherSimpleFields = new HashMap<String, String>();
// fields that, as they are should not be escaped before forwarding to the server
private Map<String, String> otherUnescapableFields = new HashMap<String, String>();
public VCard() {
}
/**
* Set generic VCard field.
*
* @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ,
* GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC.
*/
public String getField(String field) {
return otherSimpleFields.get(field);
}
/**
* Set generic VCard field.
*
* @param value value of field
* @param field field to set. See {@link #getField(String)}
* @see #getField(String)
*/
public void setField(String field, String value) {
setField(field, value, false);
}
/**
* Set generic, unescapable VCard field. If unescabale is set to true, XML maybe a part of the
* value.
*
* @param value value of field
* @param field field to set. See {@link #getField(String)}
* @param isUnescapable True if the value should not be escaped, and false if it should.
*/
public void setField(String field, String value, boolean isUnescapable) {
if (!isUnescapable) {
otherSimpleFields.put(field, value);
}
else {
otherUnescapableFields.put(field, value);
}
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
// Update FN field
updateFN();
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
// Update FN field
updateFN();
}
public String getMiddleName() {
return middleName;
}
public void setMiddleName(String middleName) {
this.middleName = middleName;
// Update FN field
updateFN();
}
public String getNickName() {
return otherSimpleFields.get("NICKNAME");
}
public void setNickName(String nickName) {
otherSimpleFields.put("NICKNAME", nickName);
}
public String getEmailHome() {
return emailHome;
}
public void setEmailHome(String email) {
this.emailHome = email;
}
public String getEmailWork() {
return emailWork;
}
public void setEmailWork(String emailWork) {
this.emailWork = emailWork;
}
public String getJabberId() {
return otherSimpleFields.get("JABBERID");
}
public void setJabberId(String jabberId) {
otherSimpleFields.put("JABBERID", jabberId);
}
public String getOrganization() {
return organization;
}
public void setOrganization(String organization) {
this.organization = organization;
}
public String getOrganizationUnit() {
return organizationUnit;
}
public void setOrganizationUnit(String organizationUnit) {
this.organizationUnit = organizationUnit;
}
/**
* Get home address field
*
* @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
* LOCALITY, REGION, PCODE, CTRY
*/
public String getAddressFieldHome(String addrField) {
return homeAddr.get(addrField);
}
/**
* Set home address field
*
* @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
* LOCALITY, REGION, PCODE, CTRY
*/
public void setAddressFieldHome(String addrField, String value) {
homeAddr.put(addrField, value);
}
/**
* Get work address field
*
* @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
* LOCALITY, REGION, PCODE, CTRY
*/
public String getAddressFieldWork(String addrField) {
return workAddr.get(addrField);
}
/**
* Set work address field
*
* @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
* LOCALITY, REGION, PCODE, CTRY
*/
public void setAddressFieldWork(String addrField, String value) {
workAddr.put(addrField, value);
}
/**
* Set home phone number
*
* @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
* @param phoneNum phone number
*/
public void setPhoneHome(String phoneType, String phoneNum) {
homePhones.put(phoneType, phoneNum);
}
/**
* Get home phone number
*
* @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
*/
public String getPhoneHome(String phoneType) {
return homePhones.get(phoneType);
}
/**
* Set work phone number
*
* @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
* @param phoneNum phone number
*/
public void setPhoneWork(String phoneType, String phoneNum) {
workPhones.put(phoneType, phoneNum);
}
/**
* Get work phone number
*
* @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
*/
public String getPhoneWork(String phoneType) {
return workPhones.get(phoneType);
}
/**
* Set the avatar for the VCard by specifying the url to the image.
*
* @param avatarURL the url to the image(png,jpeg,gif,bmp)
*/
public void setAvatar(URL avatarURL) {
byte[] bytes = new byte[0];
try {
bytes = getBytes(avatarURL);
}
catch (IOException e) {
e.printStackTrace();
}
setAvatar(bytes);
}
/**
* Removes the avatar from the vCard
*
* This is done by setting the PHOTO value to the empty string as defined in XEP-0153
*/
public void removeAvatar() {
// Remove avatar (if any)
photoBinval = null;
photoMimeType = null;
}
/**
* Specify the bytes of the JPEG for the avatar to use.
* If bytes is null, then the avatar will be removed.
* 'image/jpeg' will be used as MIME type.
*
* @param bytes the bytes of the avatar, or null to remove the avatar data
*/
public void setAvatar(byte[] bytes) {
setAvatar(bytes, "image/jpeg");
}
/**
* Specify the bytes for the avatar to use as well as the mime type.
*
* @param bytes the bytes of the avatar.
* @param mimeType the mime type of the avatar.
*/
public void setAvatar(byte[] bytes, String mimeType) {
// If bytes is null, remove the avatar
if (bytes == null) {
removeAvatar();
return;
}
// Otherwise, add to mappings.
String encodedImage = StringUtils.encodeBase64(bytes);
setAvatar(encodedImage, mimeType);
}
/**
* Specify the Avatar used for this vCard.
*
* @param encodedImage the Base64 encoded image as String
* @param mimeType the MIME type of the image
*/
public void setAvatar(String encodedImage, String mimeType) {
photoBinval = encodedImage;
photoMimeType = mimeType;
}
/**
* Return the byte representation of the avatar(if one exists), otherwise returns null if
* no avatar could be found.
* <b>Example 1</b>
* <pre>
* // Load Avatar from VCard
* byte[] avatarBytes = vCard.getAvatar();
* <p/>
* // To create an ImageIcon for Swing applications
* ImageIcon icon = new ImageIcon(avatar);
* <p/>
* // To create just an image object from the bytes
* ByteArrayInputStream bais = new ByteArrayInputStream(avatar);
* try {
* Image image = ImageIO.read(bais);
* }
* catch (IOException e) {
* e.printStackTrace();
* }
* </pre>
*
* @return byte representation of avatar.
*/
public byte[] getAvatar() {
if (photoBinval == null) {
return null;
}
return StringUtils.decodeBase64(photoBinval);
}
/**
* Returns the MIME Type of the avatar or null if none is set
*
* @return the MIME Type of the avatar or null
*/
public String getAvatarMimeType() {
return photoMimeType;
}
/**
* Common code for getting the bytes of a url.
*
* @param url the url to read.
*/
public static byte[] getBytes(URL url) throws IOException {
final String path = url.getPath();
final File file = new File(path);
if (file.exists()) {
return getFileBytes(file);
}
return null;
}
private static byte[] getFileBytes(File file) throws IOException {
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(file));
int bytes = (int) file.length();
byte[] buffer = new byte[bytes];
int readBytes = bis.read(buffer);
if (readBytes != buffer.length) {
throw new IOException("Entire file not read");
}
return buffer;
}
finally {
if (bis != null) {
bis.close();
}
}
}
/**
* Returns the SHA-1 Hash of the Avatar image.
*
* @return the SHA-1 Hash of the Avatar image.
*/
public String getAvatarHash() {
byte[] bytes = getAvatar();
if (bytes == null) {
return null;
}
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-1");
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
digest.update(bytes);
return StringUtils.encodeHex(digest.digest());
}
private void updateFN() {
StringBuilder sb = new StringBuilder();
if (firstName != null) {
sb.append(StringUtils.escapeForXML(firstName)).append(' ');
}
if (middleName != null) {
sb.append(StringUtils.escapeForXML(middleName)).append(' ');
}
if (lastName != null) {
sb.append(StringUtils.escapeForXML(lastName));
}
setField("FN", sb.toString());
}
/**
* Save this vCard for the user connected by 'connection'. Connection should be authenticated
* and not anonymous.<p>
* <p/>
* NOTE: the method is asynchronous and does not wait for the returned value.
*
* @param connection the Connection to use.
* @throws XMPPException thrown if there was an issue setting the VCard in the server.
*/
public void save(Connection connection) throws XMPPException {
checkAuthenticated(connection, true);
setType(IQ.Type.SET);
setFrom(connection.getUser());
PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(getPacketID()));
connection.sendPacket(this);
Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Cancel the collector.
collector.cancel();
if (response == null) {
throw new XMPPException("No response from server on status set.");
}
if (response.getError() != null) {
throw new XMPPException(response.getError());
}
}
/**
* Load VCard information for a connected user. Connection should be authenticated
* and not anonymous.
*/
public void load(Connection connection) throws XMPPException {
checkAuthenticated(connection, true);
setFrom(connection.getUser());
doLoad(connection, connection.getUser());
}
/**
* Load VCard information for a given user. Connection should be authenticated and not anonymous.
*/
public void load(Connection connection, String user) throws XMPPException {
checkAuthenticated(connection, false);
setTo(user);
doLoad(connection, user);
}
private void doLoad(Connection connection, String user) throws XMPPException {
setType(Type.GET);
PacketCollector collector = connection.createPacketCollector(
new PacketIDFilter(getPacketID()));
connection.sendPacket(this);
VCard result = null;
try {
result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
if (result == null) {
String errorMessage = "Timeout getting VCard information";
throw new XMPPException(errorMessage, new XMPPError(
XMPPError.Condition.request_timeout, errorMessage));
}
if (result.getError() != null) {
throw new XMPPException(result.getError());
}
}
catch (ClassCastException e) {
System.out.println("No VCard for " + user);
}
copyFieldsFrom(result);
}
public String getChildElementXML() {
StringBuilder sb = new StringBuilder();
new VCardWriter(sb).write();
return sb.toString();
}
private void copyFieldsFrom(VCard from) {
Field[] fields = VCard.class.getDeclaredFields();
for (Field field : fields) {
if (field.getDeclaringClass() == VCard.class &&
!Modifier.isFinal(field.getModifiers())) {
try {
field.setAccessible(true);
field.set(this, field.get(from));
}
catch (IllegalAccessException e) {
throw new RuntimeException("This cannot happen:" + field, e);
}
}
}
}
private void checkAuthenticated(Connection connection, boolean checkForAnonymous) {
if (connection == null) {
throw new IllegalArgumentException("No connection was provided");
}
if (!connection.isAuthenticated()) {
throw new IllegalArgumentException("Connection is not authenticated");
}
if (checkForAnonymous && connection.isAnonymous()) {
throw new IllegalArgumentException("Connection cannot be anonymous");
}
}
private boolean hasContent() {
//noinspection OverlyComplexBooleanExpression
return hasNameField()
|| hasOrganizationFields()
|| emailHome != null
|| emailWork != null
|| otherSimpleFields.size() > 0
|| otherUnescapableFields.size() > 0
|| homeAddr.size() > 0
|| homePhones.size() > 0
|| workAddr.size() > 0
|| workPhones.size() > 0
|| photoBinval != null
;
}
private boolean hasNameField() {
return firstName != null || lastName != null || middleName != null;
}
private boolean hasOrganizationFields() {
return organization != null || organizationUnit != null;
}
// Used in tests:
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final VCard vCard = (VCard) o;
if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) {
return false;
}
if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) {
return false;
}
if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) {
return false;
}
if (!homeAddr.equals(vCard.homeAddr)) {
return false;
}
if (!homePhones.equals(vCard.homePhones)) {
return false;
}
if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) {
return false;
}
if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) {
return false;
}
if (organization != null ?
!organization.equals(vCard.organization) : vCard.organization != null) {
return false;
}
if (organizationUnit != null ?
!organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) {
return false;
}
if (!otherSimpleFields.equals(vCard.otherSimpleFields)) {
return false;
}
if (!workAddr.equals(vCard.workAddr)) {
return false;
}
if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) {
return false;
}
return workPhones.equals(vCard.workPhones);
}
public int hashCode() {
int result;
result = homePhones.hashCode();
result = 29 * result + workPhones.hashCode();
result = 29 * result + homeAddr.hashCode();
result = 29 * result + workAddr.hashCode();
result = 29 * result + (firstName != null ? firstName.hashCode() : 0);
result = 29 * result + (lastName != null ? lastName.hashCode() : 0);
result = 29 * result + (middleName != null ? middleName.hashCode() : 0);
result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0);
result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0);
result = 29 * result + (organization != null ? organization.hashCode() : 0);
result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0);
result = 29 * result + otherSimpleFields.hashCode();
result = 29 * result + (photoBinval != null ? photoBinval.hashCode() : 0);
return result;
}
public String toString() {
return getChildElementXML();
}
//==============================================================
private class VCardWriter {
private final StringBuilder sb;
VCardWriter(StringBuilder sb) {
this.sb = sb;
}
public void write() {
appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() {
public void addTagContent() {
buildActualContent();
}
});
}
private void buildActualContent() {
if (hasNameField()) {
appendN();
}
appendOrganization();
appendGenericFields();
appendPhoto();
appendEmail(emailWork, "WORK");
appendEmail(emailHome, "HOME");
appendPhones(workPhones, "WORK");
appendPhones(homePhones, "HOME");
appendAddress(workAddr, "WORK");
appendAddress(homeAddr, "HOME");
}
private void appendPhoto() {
if (photoBinval == null)
return;
appendTag("PHOTO", true, new ContentBuilder() {
public void addTagContent() {
appendTag("BINVAL", photoBinval); // No need to escape photoBinval, as it's already Base64 encoded
appendTag("TYPE", StringUtils.escapeForXML(photoMimeType));
}
});
}
private void appendEmail(final String email, final String type) {
if (email != null) {
appendTag("EMAIL", true, new ContentBuilder() {
public void addTagContent() {
appendEmptyTag(type);
appendEmptyTag("INTERNET");
appendEmptyTag("PREF");
appendTag("USERID", StringUtils.escapeForXML(email));
}
});
}
}
private void appendPhones(Map<String, String> phones, final String code) {
Iterator<Map.Entry<String, String>> it = phones.entrySet().iterator();
while (it.hasNext()) {
final Map.Entry<String,String> entry = it.next();
appendTag("TEL", true, new ContentBuilder() {
public void addTagContent() {
appendEmptyTag(entry.getKey());
appendEmptyTag(code);
appendTag("NUMBER", StringUtils.escapeForXML(entry.getValue()));
}
});
}
}
private void appendAddress(final Map<String, String> addr, final String code) {
if (addr.size() > 0) {
appendTag("ADR", true, new ContentBuilder() {
public void addTagContent() {
appendEmptyTag(code);
Iterator<Map.Entry<String, String>> it = addr.entrySet().iterator();
while (it.hasNext()) {
final Entry<String, String> entry = it.next();
appendTag(entry.getKey(), StringUtils.escapeForXML(entry.getValue()));
}
}
});
}
}
private void appendEmptyTag(Object tag) {
sb.append('<').append(tag).append("/>");
}
private void appendGenericFields() {
Iterator<Map.Entry<String, String>> it = otherSimpleFields.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
appendTag(entry.getKey().toString(),
StringUtils.escapeForXML(entry.getValue()));
}
it = otherUnescapableFields.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
appendTag(entry.getKey().toString(),entry.getValue());
}
}
private void appendOrganization() {
if (hasOrganizationFields()) {
appendTag("ORG", true, new ContentBuilder() {
public void addTagContent() {
appendTag("ORGNAME", StringUtils.escapeForXML(organization));
appendTag("ORGUNIT", StringUtils.escapeForXML(organizationUnit));
}
});
}
}
private void appendN() {
appendTag("N", true, new ContentBuilder() {
public void addTagContent() {
appendTag("FAMILY", StringUtils.escapeForXML(lastName));
appendTag("GIVEN", StringUtils.escapeForXML(firstName));
appendTag("MIDDLE", StringUtils.escapeForXML(middleName));
}
});
}
private void appendTag(String tag, String attr, String attrValue, boolean hasContent,
ContentBuilder builder) {
sb.append('<').append(tag);
if (attr != null) {
sb.append(' ').append(attr).append('=').append('\'').append(attrValue).append('\'');
}
if (hasContent) {
sb.append('>');
builder.addTagContent();
sb.append("</").append(tag).append(">\n");
}
else {
sb.append("/>\n");
}
}
private void appendTag(String tag, boolean hasContent, ContentBuilder builder) {
appendTag(tag, null, null, hasContent, builder);
}
private void appendTag(String tag, final String tagText) {
if (tagText == null) return;
final ContentBuilder contentBuilder = new ContentBuilder() {
public void addTagContent() {
sb.append(tagText.trim());
}
};
appendTag(tag, true, contentBuilder);
}
}
//==============================================================
private interface ContentBuilder {
void addTagContent();
}
//==============================================================
}