diff options
Diffstat (limited to 'src/org/jivesoftware/smackx/packet/VCard.java')
-rw-r--r-- | src/org/jivesoftware/smackx/packet/VCard.java | 883 |
1 files changed, 883 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smackx/packet/VCard.java b/src/org/jivesoftware/smackx/packet/VCard.java new file mode 100644 index 0000000..9766db8 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/VCard.java @@ -0,0 +1,883 @@ +/** + * $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(); + } + + //============================================================== +} + |