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
21package org.jivesoftware.smackx.packet;
22
23import java.io.BufferedInputStream;
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.IOException;
27import java.lang.reflect.Field;
28import java.lang.reflect.Modifier;
29import java.net.URL;
30import java.security.MessageDigest;
31import java.security.NoSuchAlgorithmException;
32import java.util.HashMap;
33import java.util.Iterator;
34import java.util.Map;
35import java.util.Map.Entry;
36
37import org.jivesoftware.smack.Connection;
38import org.jivesoftware.smack.PacketCollector;
39import org.jivesoftware.smack.SmackConfiguration;
40import org.jivesoftware.smack.XMPPException;
41import org.jivesoftware.smack.filter.PacketIDFilter;
42import org.jivesoftware.smack.packet.IQ;
43import org.jivesoftware.smack.packet.Packet;
44import org.jivesoftware.smack.packet.XMPPError;
45import 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 */
87public 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