Home | History | Annotate | Download | only in packet
      1 /**
      2  * $RCSfile$
      3  * $Revision$
      4  * $Date$
      5  *
      6  * Copyright 2003-2007 Jive Software.
      7  *
      8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      9  * you may not use this file except in compliance with the License.
     10  * You may obtain a copy of the License at
     11  *
     12  *     http://www.apache.org/licenses/LICENSE-2.0
     13  *
     14  * Unless required by applicable law or agreed to in writing, software
     15  * distributed under the License is distributed on an "AS IS" BASIS,
     16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17  * See the License for the specific language governing permissions and
     18  * limitations under the License.
     19  */
     20 
     21 package org.jivesoftware.smackx.packet;
     22 
     23 import java.io.BufferedInputStream;
     24 import java.io.File;
     25 import java.io.FileInputStream;
     26 import java.io.IOException;
     27 import java.lang.reflect.Field;
     28 import java.lang.reflect.Modifier;
     29 import java.net.URL;
     30 import java.security.MessageDigest;
     31 import java.security.NoSuchAlgorithmException;
     32 import java.util.HashMap;
     33 import java.util.Iterator;
     34 import java.util.Map;
     35 import java.util.Map.Entry;
     36 
     37 import org.jivesoftware.smack.Connection;
     38 import org.jivesoftware.smack.PacketCollector;
     39 import org.jivesoftware.smack.SmackConfiguration;
     40 import org.jivesoftware.smack.XMPPException;
     41 import org.jivesoftware.smack.filter.PacketIDFilter;
     42 import org.jivesoftware.smack.packet.IQ;
     43 import org.jivesoftware.smack.packet.Packet;
     44 import org.jivesoftware.smack.packet.XMPPError;
     45 import org.jivesoftware.smack.util.StringUtils;
     46 
     47 /**
     48  * A VCard class for use with the
     49  * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p>
     50  * <p/>
     51  * You should refer to the
     52  * <a href="http://www.jabber.org/jeps/jep-0054.html" target="_blank">JEP-54 documentation</a>.<p>
     53  * <p/>
     54  * Please note that this class is incomplete but it does provide the most commonly found
     55  * information in vCards. Also remember that VCard transfer is not a standard, and the protocol
     56  * may change or be replaced.<p>
     57  * <p/>
     58  * <b>Usage:</b>
     59  * <pre>
     60  * <p/>
     61  * // To save VCard:
     62  * <p/>
     63  * VCard vCard = new VCard();
     64  * vCard.setFirstName("kir");
     65  * vCard.setLastName("max");
     66  * vCard.setEmailHome("foo (at) fee.bar");
     67  * vCard.setJabberId("jabber (at) id.org");
     68  * vCard.setOrganization("Jetbrains, s.r.o");
     69  * vCard.setNickName("KIR");
     70  * <p/>
     71  * vCard.setField("TITLE", "Mr");
     72  * vCard.setAddressFieldHome("STREET", "Some street");
     73  * vCard.setAddressFieldWork("CTRY", "US");
     74  * vCard.setPhoneWork("FAX", "3443233");
     75  * <p/>
     76  * vCard.save(connection);
     77  * <p/>
     78  * // To load VCard:
     79  * <p/>
     80  * VCard vCard = new VCard();
     81  * vCard.load(conn); // load own VCard
     82  * vCard.load(conn, "joe (at) foo.bar"); // load someone's VCard
     83  * </pre>
     84  *
     85  * @author Kirill Maximov (kir (at) maxkir.com)
     86  */
     87 public class VCard extends IQ {
     88 
     89     /**
     90      * Phone types:
     91      * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF?
     92      */
     93     private Map<String, String> homePhones = new HashMap<String, String>();
     94     private Map<String, String> workPhones = new HashMap<String, String>();
     95 
     96 
     97     /**
     98      * Address types:
     99      * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?,
    100      * REGION?, PCODE?, CTRY?
    101      */
    102     private Map<String, String> homeAddr = new HashMap<String, String>();
    103     private Map<String, String> workAddr = new HashMap<String, String>();
    104 
    105     private String firstName;
    106     private String lastName;
    107     private String middleName;
    108 
    109     private String emailHome;
    110     private String emailWork;
    111 
    112     private String organization;
    113     private String organizationUnit;
    114 
    115     private String photoMimeType;
    116     private String photoBinval;
    117 
    118     /**
    119      * Such as DESC ROLE GEO etc.. see JEP-0054
    120      */
    121     private Map<String, String> otherSimpleFields = new HashMap<String, String>();
    122 
    123     // fields that, as they are should not be escaped before forwarding to the server
    124     private Map<String, String> otherUnescapableFields = new HashMap<String, String>();
    125 
    126     public VCard() {
    127     }
    128 
    129     /**
    130      * Set generic VCard field.
    131      *
    132      * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ,
    133      *              GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC.
    134      */
    135     public String getField(String field) {
    136         return otherSimpleFields.get(field);
    137     }
    138 
    139     /**
    140      * Set generic VCard field.
    141      *
    142      * @param value value of field
    143      * @param field field to set. See {@link #getField(String)}
    144      * @see #getField(String)
    145      */
    146     public void setField(String field, String value) {
    147         setField(field, value, false);
    148     }
    149 
    150     /**
    151      * Set generic, unescapable VCard field. If unescabale is set to true, XML maybe a part of the
    152      * value.
    153      *
    154      * @param value         value of field
    155      * @param field         field to set. See {@link #getField(String)}
    156      * @param isUnescapable True if the value should not be escaped, and false if it should.
    157      */
    158     public void setField(String field, String value, boolean isUnescapable) {
    159         if (!isUnescapable) {
    160             otherSimpleFields.put(field, value);
    161         }
    162         else {
    163             otherUnescapableFields.put(field, value);
    164         }
    165     }
    166 
    167     public String getFirstName() {
    168         return firstName;
    169     }
    170 
    171     public void setFirstName(String firstName) {
    172         this.firstName = firstName;
    173         // Update FN field
    174         updateFN();
    175     }
    176 
    177     public String getLastName() {
    178         return lastName;
    179     }
    180 
    181     public void setLastName(String lastName) {
    182         this.lastName = lastName;
    183         // Update FN field
    184         updateFN();
    185     }
    186 
    187     public String getMiddleName() {
    188         return middleName;
    189     }
    190 
    191     public void setMiddleName(String middleName) {
    192         this.middleName = middleName;
    193         // Update FN field
    194         updateFN();
    195     }
    196 
    197     public String getNickName() {
    198         return otherSimpleFields.get("NICKNAME");
    199     }
    200 
    201     public void setNickName(String nickName) {
    202         otherSimpleFields.put("NICKNAME", nickName);
    203     }
    204 
    205     public String getEmailHome() {
    206         return emailHome;
    207     }
    208 
    209     public void setEmailHome(String email) {
    210         this.emailHome = email;
    211     }
    212 
    213     public String getEmailWork() {
    214         return emailWork;
    215     }
    216 
    217     public void setEmailWork(String emailWork) {
    218         this.emailWork = emailWork;
    219     }
    220 
    221     public String getJabberId() {
    222         return otherSimpleFields.get("JABBERID");
    223     }
    224 
    225     public void setJabberId(String jabberId) {
    226         otherSimpleFields.put("JABBERID", jabberId);
    227     }
    228 
    229     public String getOrganization() {
    230         return organization;
    231     }
    232 
    233     public void setOrganization(String organization) {
    234         this.organization = organization;
    235     }
    236 
    237     public String getOrganizationUnit() {
    238         return organizationUnit;
    239     }
    240 
    241     public void setOrganizationUnit(String organizationUnit) {
    242         this.organizationUnit = organizationUnit;
    243     }
    244 
    245     /**
    246      * Get home address field
    247      *
    248      * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
    249      *                  LOCALITY, REGION, PCODE, CTRY
    250      */
    251     public String getAddressFieldHome(String addrField) {
    252         return homeAddr.get(addrField);
    253     }
    254 
    255     /**
    256      * Set home address field
    257      *
    258      * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
    259      *                  LOCALITY, REGION, PCODE, CTRY
    260      */
    261     public void setAddressFieldHome(String addrField, String value) {
    262         homeAddr.put(addrField, value);
    263     }
    264 
    265     /**
    266      * Get work address field
    267      *
    268      * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
    269      *                  LOCALITY, REGION, PCODE, CTRY
    270      */
    271     public String getAddressFieldWork(String addrField) {
    272         return workAddr.get(addrField);
    273     }
    274 
    275     /**
    276      * Set work address field
    277      *
    278      * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
    279      *                  LOCALITY, REGION, PCODE, CTRY
    280      */
    281     public void setAddressFieldWork(String addrField, String value) {
    282         workAddr.put(addrField, value);
    283     }
    284 
    285 
    286     /**
    287      * Set home phone number
    288      *
    289      * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
    290      * @param phoneNum  phone number
    291      */
    292     public void setPhoneHome(String phoneType, String phoneNum) {
    293         homePhones.put(phoneType, phoneNum);
    294     }
    295 
    296     /**
    297      * Get home phone number
    298      *
    299      * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
    300      */
    301     public String getPhoneHome(String phoneType) {
    302         return homePhones.get(phoneType);
    303     }
    304 
    305     /**
    306      * Set work phone number
    307      *
    308      * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
    309      * @param phoneNum  phone number
    310      */
    311     public void setPhoneWork(String phoneType, String phoneNum) {
    312         workPhones.put(phoneType, phoneNum);
    313     }
    314 
    315     /**
    316      * Get work phone number
    317      *
    318      * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
    319      */
    320     public String getPhoneWork(String phoneType) {
    321         return workPhones.get(phoneType);
    322     }
    323 
    324     /**
    325      * Set the avatar for the VCard by specifying the url to the image.
    326      *
    327      * @param avatarURL the url to the image(png,jpeg,gif,bmp)
    328      */
    329     public void setAvatar(URL avatarURL) {
    330         byte[] bytes = new byte[0];
    331         try {
    332             bytes = getBytes(avatarURL);
    333         }
    334         catch (IOException e) {
    335             e.printStackTrace();
    336         }
    337 
    338         setAvatar(bytes);
    339     }
    340 
    341     /**
    342      * Removes the avatar from the vCard
    343      *
    344      *  This is done by setting the PHOTO value to the empty string as defined in XEP-0153
    345      */
    346     public void removeAvatar() {
    347         // Remove avatar (if any)
    348         photoBinval = null;
    349         photoMimeType = null;
    350     }
    351 
    352     /**
    353      * Specify the bytes of the JPEG for the avatar to use.
    354      * If bytes is null, then the avatar will be removed.
    355      * 'image/jpeg' will be used as MIME type.
    356      *
    357      * @param bytes the bytes of the avatar, or null to remove the avatar data
    358      */
    359     public void setAvatar(byte[] bytes) {
    360         setAvatar(bytes, "image/jpeg");
    361     }
    362 
    363     /**
    364      * Specify the bytes for the avatar to use as well as the mime type.
    365      *
    366      * @param bytes the bytes of the avatar.
    367      * @param mimeType the mime type of the avatar.
    368      */
    369     public void setAvatar(byte[] bytes, String mimeType) {
    370         // If bytes is null, remove the avatar
    371         if (bytes == null) {
    372             removeAvatar();
    373             return;
    374         }
    375 
    376         // Otherwise, add to mappings.
    377         String encodedImage = StringUtils.encodeBase64(bytes);
    378 
    379         setAvatar(encodedImage, mimeType);
    380     }
    381 
    382     /**
    383      * Specify the Avatar used for this vCard.
    384      *
    385      * @param encodedImage the Base64 encoded image as String
    386      * @param mimeType the MIME type of the image
    387      */
    388     public void setAvatar(String encodedImage, String mimeType) {
    389         photoBinval = encodedImage;
    390         photoMimeType = mimeType;
    391     }
    392 
    393     /**
    394      * Return the byte representation of the avatar(if one exists), otherwise returns null if
    395      * no avatar could be found.
    396      * <b>Example 1</b>
    397      * <pre>
    398      * // Load Avatar from VCard
    399      * byte[] avatarBytes = vCard.getAvatar();
    400      * <p/>
    401      * // To create an ImageIcon for Swing applications
    402      * ImageIcon icon = new ImageIcon(avatar);
    403      * <p/>
    404      * // To create just an image object from the bytes
    405      * ByteArrayInputStream bais = new ByteArrayInputStream(avatar);
    406      * try {
    407      *   Image image = ImageIO.read(bais);
    408      *  }
    409      *  catch (IOException e) {
    410      *    e.printStackTrace();
    411      * }
    412      * </pre>
    413      *
    414      * @return byte representation of avatar.
    415      */
    416     public byte[] getAvatar() {
    417         if (photoBinval == null) {
    418             return null;
    419         }
    420         return StringUtils.decodeBase64(photoBinval);
    421     }
    422 
    423     /**
    424      * Returns the MIME Type of the avatar or null if none is set
    425      *
    426      * @return the MIME Type of the avatar or null
    427      */
    428     public String getAvatarMimeType() {
    429         return photoMimeType;
    430     }
    431 
    432     /**
    433      * Common code for getting the bytes of a url.
    434      *
    435      * @param url the url to read.
    436      */
    437     public static byte[] getBytes(URL url) throws IOException {
    438         final String path = url.getPath();
    439         final File file = new File(path);
    440         if (file.exists()) {
    441             return getFileBytes(file);
    442         }
    443 
    444         return null;
    445     }
    446 
    447     private static byte[] getFileBytes(File file) throws IOException {
    448         BufferedInputStream bis = null;
    449         try {
    450             bis = new BufferedInputStream(new FileInputStream(file));
    451             int bytes = (int) file.length();
    452             byte[] buffer = new byte[bytes];
    453             int readBytes = bis.read(buffer);
    454             if (readBytes != buffer.length) {
    455                 throw new IOException("Entire file not read");
    456             }
    457             return buffer;
    458         }
    459         finally {
    460             if (bis != null) {
    461                 bis.close();
    462             }
    463         }
    464     }
    465 
    466     /**
    467      * Returns the SHA-1 Hash of the Avatar image.
    468      *
    469      * @return the SHA-1 Hash of the Avatar image.
    470      */
    471     public String getAvatarHash() {
    472         byte[] bytes = getAvatar();
    473         if (bytes == null) {
    474             return null;
    475         }
    476 
    477         MessageDigest digest;
    478         try {
    479             digest = MessageDigest.getInstance("SHA-1");
    480         }
    481         catch (NoSuchAlgorithmException e) {
    482             e.printStackTrace();
    483             return null;
    484         }
    485 
    486         digest.update(bytes);
    487         return StringUtils.encodeHex(digest.digest());
    488     }
    489 
    490     private void updateFN() {
    491         StringBuilder sb = new StringBuilder();
    492         if (firstName != null) {
    493             sb.append(StringUtils.escapeForXML(firstName)).append(' ');
    494         }
    495         if (middleName != null) {
    496             sb.append(StringUtils.escapeForXML(middleName)).append(' ');
    497         }
    498         if (lastName != null) {
    499             sb.append(StringUtils.escapeForXML(lastName));
    500         }
    501         setField("FN", sb.toString());
    502     }
    503 
    504     /**
    505      * Save this vCard for the user connected by 'connection'. Connection should be authenticated
    506      * and not anonymous.<p>
    507      * <p/>
    508      * NOTE: the method is asynchronous and does not wait for the returned value.
    509      *
    510      * @param connection the Connection to use.
    511      * @throws XMPPException thrown if there was an issue setting the VCard in the server.
    512      */
    513     public void save(Connection connection) throws XMPPException {
    514         checkAuthenticated(connection, true);
    515 
    516         setType(IQ.Type.SET);
    517         setFrom(connection.getUser());
    518         PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(getPacketID()));
    519         connection.sendPacket(this);
    520 
    521         Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    522 
    523         // Cancel the collector.
    524         collector.cancel();
    525         if (response == null) {
    526             throw new XMPPException("No response from server on status set.");
    527         }
    528         if (response.getError() != null) {
    529             throw new XMPPException(response.getError());
    530         }
    531     }
    532 
    533     /**
    534      * Load VCard information for a connected user. Connection should be authenticated
    535      * and not anonymous.
    536      */
    537     public void load(Connection connection) throws XMPPException {
    538         checkAuthenticated(connection, true);
    539 
    540         setFrom(connection.getUser());
    541         doLoad(connection, connection.getUser());
    542     }
    543 
    544     /**
    545      * Load VCard information for a given user. Connection should be authenticated and not anonymous.
    546      */
    547     public void load(Connection connection, String user) throws XMPPException {
    548         checkAuthenticated(connection, false);
    549 
    550         setTo(user);
    551         doLoad(connection, user);
    552     }
    553 
    554     private void doLoad(Connection connection, String user) throws XMPPException {
    555         setType(Type.GET);
    556         PacketCollector collector = connection.createPacketCollector(
    557                 new PacketIDFilter(getPacketID()));
    558         connection.sendPacket(this);
    559 
    560         VCard result = null;
    561         try {
    562             result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    563 
    564             if (result == null) {
    565                 String errorMessage = "Timeout getting VCard information";
    566                 throw new XMPPException(errorMessage, new XMPPError(
    567                         XMPPError.Condition.request_timeout, errorMessage));
    568             }
    569             if (result.getError() != null) {
    570                 throw new XMPPException(result.getError());
    571             }
    572         }
    573         catch (ClassCastException e) {
    574             System.out.println("No VCard for " + user);
    575         }
    576 
    577         copyFieldsFrom(result);
    578     }
    579 
    580     public String getChildElementXML() {
    581         StringBuilder sb = new StringBuilder();
    582         new VCardWriter(sb).write();
    583         return sb.toString();
    584     }
    585 
    586     private void copyFieldsFrom(VCard from) {
    587         Field[] fields = VCard.class.getDeclaredFields();
    588         for (Field field : fields) {
    589             if (field.getDeclaringClass() == VCard.class &&
    590                     !Modifier.isFinal(field.getModifiers())) {
    591                 try {
    592                     field.setAccessible(true);
    593                     field.set(this, field.get(from));
    594                 }
    595                 catch (IllegalAccessException e) {
    596                     throw new RuntimeException("This cannot happen:" + field, e);
    597                 }
    598             }
    599         }
    600     }
    601 
    602     private void checkAuthenticated(Connection connection, boolean checkForAnonymous) {
    603         if (connection == null) {
    604             throw new IllegalArgumentException("No connection was provided");
    605         }
    606         if (!connection.isAuthenticated()) {
    607             throw new IllegalArgumentException("Connection is not authenticated");
    608         }
    609         if (checkForAnonymous && connection.isAnonymous()) {
    610             throw new IllegalArgumentException("Connection cannot be anonymous");
    611         }
    612     }
    613 
    614     private boolean hasContent() {
    615         //noinspection OverlyComplexBooleanExpression
    616         return hasNameField()
    617                 || hasOrganizationFields()
    618                 || emailHome != null
    619                 || emailWork != null
    620                 || otherSimpleFields.size() > 0
    621                 || otherUnescapableFields.size() > 0
    622                 || homeAddr.size() > 0
    623                 || homePhones.size() > 0
    624                 || workAddr.size() > 0
    625                 || workPhones.size() > 0
    626                 || photoBinval != null
    627                 ;
    628     }
    629 
    630     private boolean hasNameField() {
    631         return firstName != null || lastName != null || middleName != null;
    632     }
    633 
    634     private boolean hasOrganizationFields() {
    635         return organization != null || organizationUnit != null;
    636     }
    637 
    638     // Used in tests:
    639 
    640     public boolean equals(Object o) {
    641         if (this == o) return true;
    642         if (o == null || getClass() != o.getClass()) return false;
    643 
    644         final VCard vCard = (VCard) o;
    645 
    646         if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) {
    647             return false;
    648         }
    649         if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) {
    650             return false;
    651         }
    652         if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) {
    653             return false;
    654         }
    655         if (!homeAddr.equals(vCard.homeAddr)) {
    656             return false;
    657         }
    658         if (!homePhones.equals(vCard.homePhones)) {
    659             return false;
    660         }
    661         if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) {
    662             return false;
    663         }
    664         if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) {
    665             return false;
    666         }
    667         if (organization != null ?
    668                 !organization.equals(vCard.organization) : vCard.organization != null) {
    669             return false;
    670         }
    671         if (organizationUnit != null ?
    672                 !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) {
    673             return false;
    674         }
    675         if (!otherSimpleFields.equals(vCard.otherSimpleFields)) {
    676             return false;
    677         }
    678         if (!workAddr.equals(vCard.workAddr)) {
    679             return false;
    680         }
    681         if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) {
    682             return false;
    683         }
    684 
    685         return workPhones.equals(vCard.workPhones);
    686     }
    687 
    688     public int hashCode() {
    689         int result;
    690         result = homePhones.hashCode();
    691         result = 29 * result + workPhones.hashCode();
    692         result = 29 * result + homeAddr.hashCode();
    693         result = 29 * result + workAddr.hashCode();
    694         result = 29 * result + (firstName != null ? firstName.hashCode() : 0);
    695         result = 29 * result + (lastName != null ? lastName.hashCode() : 0);
    696         result = 29 * result + (middleName != null ? middleName.hashCode() : 0);
    697         result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0);
    698         result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0);
    699         result = 29 * result + (organization != null ? organization.hashCode() : 0);
    700         result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0);
    701         result = 29 * result + otherSimpleFields.hashCode();
    702         result = 29 * result + (photoBinval != null ? photoBinval.hashCode() : 0);
    703         return result;
    704     }
    705 
    706     public String toString() {
    707         return getChildElementXML();
    708     }
    709 
    710     //==============================================================
    711 
    712     private class VCardWriter {
    713 
    714         private final StringBuilder sb;
    715 
    716         VCardWriter(StringBuilder sb) {
    717             this.sb = sb;
    718         }
    719 
    720         public void write() {
    721             appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() {
    722                 public void addTagContent() {
    723                     buildActualContent();
    724                 }
    725             });
    726         }
    727 
    728         private void buildActualContent() {
    729             if (hasNameField()) {
    730                 appendN();
    731             }
    732 
    733             appendOrganization();
    734             appendGenericFields();
    735             appendPhoto();
    736 
    737             appendEmail(emailWork, "WORK");
    738             appendEmail(emailHome, "HOME");
    739 
    740             appendPhones(workPhones, "WORK");
    741             appendPhones(homePhones, "HOME");
    742 
    743             appendAddress(workAddr, "WORK");
    744             appendAddress(homeAddr, "HOME");
    745         }
    746 
    747         private void appendPhoto() {
    748             if (photoBinval == null)
    749                 return;
    750 
    751             appendTag("PHOTO", true, new ContentBuilder() {
    752                 public void addTagContent() {
    753                     appendTag("BINVAL", photoBinval); // No need to escape photoBinval, as it's already Base64 encoded
    754                     appendTag("TYPE", StringUtils.escapeForXML(photoMimeType));
    755                 }
    756             });
    757         }
    758         private void appendEmail(final String email, final String type) {
    759             if (email != null) {
    760                 appendTag("EMAIL", true, new ContentBuilder() {
    761                     public void addTagContent() {
    762                         appendEmptyTag(type);
    763                         appendEmptyTag("INTERNET");
    764                         appendEmptyTag("PREF");
    765                         appendTag("USERID", StringUtils.escapeForXML(email));
    766                     }
    767                 });
    768             }
    769         }
    770 
    771         private void appendPhones(Map<String, String> phones, final String code) {
    772             Iterator<Map.Entry<String, String>> it = phones.entrySet().iterator();
    773             while (it.hasNext()) {
    774                 final Map.Entry<String,String> entry = it.next();
    775                 appendTag("TEL", true, new ContentBuilder() {
    776                     public void addTagContent() {
    777                         appendEmptyTag(entry.getKey());
    778                         appendEmptyTag(code);
    779                         appendTag("NUMBER", StringUtils.escapeForXML(entry.getValue()));
    780                     }
    781                 });
    782             }
    783         }
    784 
    785         private void appendAddress(final Map<String, String> addr, final String code) {
    786             if (addr.size() > 0) {
    787                 appendTag("ADR", true, new ContentBuilder() {
    788                     public void addTagContent() {
    789                         appendEmptyTag(code);
    790 
    791                         Iterator<Map.Entry<String, String>> it = addr.entrySet().iterator();
    792                         while (it.hasNext()) {
    793                             final Entry<String, String> entry = it.next();
    794                             appendTag(entry.getKey(), StringUtils.escapeForXML(entry.getValue()));
    795                         }
    796                     }
    797                 });
    798             }
    799         }
    800 
    801         private void appendEmptyTag(Object tag) {
    802             sb.append('<').append(tag).append("/>");
    803         }
    804 
    805         private void appendGenericFields() {
    806             Iterator<Map.Entry<String, String>> it = otherSimpleFields.entrySet().iterator();
    807             while (it.hasNext()) {
    808                 Map.Entry<String, String> entry = it.next();
    809                 appendTag(entry.getKey().toString(),
    810                         StringUtils.escapeForXML(entry.getValue()));
    811             }
    812 
    813             it = otherUnescapableFields.entrySet().iterator();
    814             while (it.hasNext()) {
    815                 Map.Entry<String, String> entry = it.next();
    816                 appendTag(entry.getKey().toString(),entry.getValue());
    817             }
    818         }
    819 
    820         private void appendOrganization() {
    821             if (hasOrganizationFields()) {
    822                 appendTag("ORG", true, new ContentBuilder() {
    823                     public void addTagContent() {
    824                         appendTag("ORGNAME", StringUtils.escapeForXML(organization));
    825                         appendTag("ORGUNIT", StringUtils.escapeForXML(organizationUnit));
    826                     }
    827                 });
    828             }
    829         }
    830 
    831         private void appendN() {
    832             appendTag("N", true, new ContentBuilder() {
    833                 public void addTagContent() {
    834                     appendTag("FAMILY", StringUtils.escapeForXML(lastName));
    835                     appendTag("GIVEN", StringUtils.escapeForXML(firstName));
    836                     appendTag("MIDDLE", StringUtils.escapeForXML(middleName));
    837                 }
    838             });
    839         }
    840 
    841         private void appendTag(String tag, String attr, String attrValue, boolean hasContent,
    842                 ContentBuilder builder) {
    843             sb.append('<').append(tag);
    844             if (attr != null) {
    845                 sb.append(' ').append(attr).append('=').append('\'').append(attrValue).append('\'');
    846             }
    847 
    848             if (hasContent) {
    849                 sb.append('>');
    850                 builder.addTagContent();
    851                 sb.append("</").append(tag).append(">\n");
    852             }
    853             else {
    854                 sb.append("/>\n");
    855             }
    856         }
    857 
    858         private void appendTag(String tag, boolean hasContent, ContentBuilder builder) {
    859             appendTag(tag, null, null, hasContent, builder);
    860         }
    861 
    862         private void appendTag(String tag, final String tagText) {
    863             if (tagText == null) return;
    864             final ContentBuilder contentBuilder = new ContentBuilder() {
    865                 public void addTagContent() {
    866                     sb.append(tagText.trim());
    867                 }
    868             };
    869             appendTag(tag, true, contentBuilder);
    870         }
    871 
    872     }
    873 
    874     //==============================================================
    875 
    876     private interface ContentBuilder {
    877 
    878         void addTagContent();
    879     }
    880 
    881     //==============================================================
    882 }
    883 
    884