• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@fee.bar");
67  * vCard.setJabberId("jabber@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@foo.bar"); // load someone's VCard
83  * </pre>
84  *
85  * @author Kirill Maximov (kir@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 
VCard()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      */
getField(String field)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      */
setField(String field, String value)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      */
setField(String field, String value, boolean isUnescapable)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 
getFirstName()167     public String getFirstName() {
168         return firstName;
169     }
170 
setFirstName(String firstName)171     public void setFirstName(String firstName) {
172         this.firstName = firstName;
173         // Update FN field
174         updateFN();
175     }
176 
getLastName()177     public String getLastName() {
178         return lastName;
179     }
180 
setLastName(String lastName)181     public void setLastName(String lastName) {
182         this.lastName = lastName;
183         // Update FN field
184         updateFN();
185     }
186 
getMiddleName()187     public String getMiddleName() {
188         return middleName;
189     }
190 
setMiddleName(String middleName)191     public void setMiddleName(String middleName) {
192         this.middleName = middleName;
193         // Update FN field
194         updateFN();
195     }
196 
getNickName()197     public String getNickName() {
198         return otherSimpleFields.get("NICKNAME");
199     }
200 
setNickName(String nickName)201     public void setNickName(String nickName) {
202         otherSimpleFields.put("NICKNAME", nickName);
203     }
204 
getEmailHome()205     public String getEmailHome() {
206         return emailHome;
207     }
208 
setEmailHome(String email)209     public void setEmailHome(String email) {
210         this.emailHome = email;
211     }
212 
getEmailWork()213     public String getEmailWork() {
214         return emailWork;
215     }
216 
setEmailWork(String emailWork)217     public void setEmailWork(String emailWork) {
218         this.emailWork = emailWork;
219     }
220 
getJabberId()221     public String getJabberId() {
222         return otherSimpleFields.get("JABBERID");
223     }
224 
setJabberId(String jabberId)225     public void setJabberId(String jabberId) {
226         otherSimpleFields.put("JABBERID", jabberId);
227     }
228 
getOrganization()229     public String getOrganization() {
230         return organization;
231     }
232 
setOrganization(String organization)233     public void setOrganization(String organization) {
234         this.organization = organization;
235     }
236 
getOrganizationUnit()237     public String getOrganizationUnit() {
238         return organizationUnit;
239     }
240 
setOrganizationUnit(String organizationUnit)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      */
getAddressFieldHome(String addrField)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      */
setAddressFieldHome(String addrField, String value)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      */
getAddressFieldWork(String addrField)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      */
setAddressFieldWork(String addrField, String value)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      */
setPhoneHome(String phoneType, String phoneNum)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      */
getPhoneHome(String phoneType)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      */
setPhoneWork(String phoneType, String phoneNum)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      */
getPhoneWork(String phoneType)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      */
setAvatar(URL avatarURL)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      */
removeAvatar()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      */
setAvatar(byte[] bytes)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      */
setAvatar(byte[] bytes, String mimeType)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      */
setAvatar(String encodedImage, String mimeType)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      */
getAvatar()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      */
getAvatarMimeType()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      */
getBytes(URL url)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 
getFileBytes(File file)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      */
getAvatarHash()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 
updateFN()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      */
save(Connection connection)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      */
load(Connection connection)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      */
load(Connection connection, String user)547     public void load(Connection connection, String user) throws XMPPException {
548         checkAuthenticated(connection, false);
549 
550         setTo(user);
551         doLoad(connection, user);
552     }
553 
doLoad(Connection connection, String user)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 
getChildElementXML()580     public String getChildElementXML() {
581         StringBuilder sb = new StringBuilder();
582         new VCardWriter(sb).write();
583         return sb.toString();
584     }
585 
copyFieldsFrom(VCard from)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 
checkAuthenticated(Connection connection, boolean checkForAnonymous)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 
hasContent()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 
hasNameField()630     private boolean hasNameField() {
631         return firstName != null || lastName != null || middleName != null;
632     }
633 
hasOrganizationFields()634     private boolean hasOrganizationFields() {
635         return organization != null || organizationUnit != null;
636     }
637 
638     // Used in tests:
639 
equals(Object o)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 
hashCode()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 
toString()706     public String toString() {
707         return getChildElementXML();
708     }
709 
710     //==============================================================
711 
712     private class VCardWriter {
713 
714         private final StringBuilder sb;
715 
VCardWriter(StringBuilder sb)716         VCardWriter(StringBuilder sb) {
717             this.sb = sb;
718         }
719 
write()720         public void write() {
721             appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() {
722                 public void addTagContent() {
723                     buildActualContent();
724                 }
725             });
726         }
727 
buildActualContent()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 
appendPhoto()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         }
appendEmail(final String email, final String type)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 
appendPhones(Map<String, String> phones, final String code)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 
appendAddress(final Map<String, String> addr, final String code)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 
appendEmptyTag(Object tag)801         private void appendEmptyTag(Object tag) {
802             sb.append('<').append(tag).append("/>");
803         }
804 
appendGenericFields()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 
appendOrganization()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 
appendN()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 
appendTag(String tag, String attr, String attrValue, boolean hasContent, ContentBuilder builder)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 
appendTag(String tag, boolean hasContent, ContentBuilder builder)858         private void appendTag(String tag, boolean hasContent, ContentBuilder builder) {
859             appendTag(tag, null, null, hasContent, builder);
860         }
861 
appendTag(String tag, final String tagText)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 
addTagContent()878         void addTagContent();
879     }
880 
881     //==============================================================
882 }
883 
884