001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.vcardtemp.packet;
019
020import java.io.BufferedInputStream;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.lang.reflect.Field;
025import java.lang.reflect.Modifier;
026import java.net.URL;
027import java.security.MessageDigest;
028import java.security.NoSuchAlgorithmException;
029import java.util.HashMap;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034
035import org.jivesoftware.smack.SmackException.NoResponseException;
036import org.jivesoftware.smack.SmackException.NotConnectedException;
037import org.jivesoftware.smack.XMPPConnection;
038import org.jivesoftware.smack.XMPPException.XMPPErrorException;
039import org.jivesoftware.smack.packet.IQ;
040import org.jivesoftware.smack.util.StringUtils;
041import org.jivesoftware.smack.util.stringencoder.Base64;
042
043import org.jivesoftware.smackx.vcardtemp.VCardManager;
044
045import org.jxmpp.jid.EntityBareJid;
046
047/**
048 * A VCard class for use with the
049 * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p>
050 * <p/>
051 * You should refer to the
052 * <a href="http://www.xmpp.org/extensions/jep-0054.html" target="_blank">XEP-54 documentation</a>.<p>
053 * <p/>
054 * Please note that this class is incomplete but it does provide the most commonly found
055 * information in vCards. Also remember that VCard transfer is not a standard, and the protocol
056 * may change or be replaced.<p>
057 * <p/>
058 * <b>Usage:</b>
059 * <pre>
060 * <p/>
061 * // To save VCard:
062 * <p/>
063 * VCard vCard = new VCard();
064 * vCard.setFirstName("kir");
065 * vCard.setLastName("max");
066 * vCard.setEmailHome("foo@fee.bar");
067 * vCard.setJabberId("jabber@id.org");
068 * vCard.setOrganization("Jetbrains, s.r.o");
069 * vCard.setNickName("KIR");
070 * <p/>
071 * vCard.setField("TITLE", "Mr");
072 * vCard.setAddressFieldHome("STREET", "Some street");
073 * vCard.setAddressFieldWork("CTRY", "US");
074 * vCard.setPhoneWork("FAX", "3443233");
075 * <p/>
076 * vCard.save(connection);
077 * <p/>
078 * // To load VCard:
079 * <p/>
080 * VCard vCard = new VCard();
081 * vCard.load(conn); // load own VCard
082 * vCard.load(conn, "joe@foo.bar"); // load someone's VCard
083 * </pre>
084 *
085 * @author Kirill Maximov (kir@maxkir.com)
086 */
087public class VCard extends IQ {
088    public static final String ELEMENT = "vCard";
089    public static final String NAMESPACE = "vcard-temp";
090
091    private static final Logger LOGGER = Logger.getLogger(VCard.class.getName());
092
093    private static final String DEFAULT_MIME_TYPE = "image/jpeg";
094
095    /**
096     * Phone types:
097     * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF?
098     */
099    private Map<String, String> homePhones = new HashMap<String, String>();
100    private Map<String, String> workPhones = new HashMap<String, String>();
101
102    /**
103     * Address types:
104     * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?,
105     * REGION?, PCODE?, CTRY?
106     */
107    private Map<String, String> homeAddr = new HashMap<String, String>();
108    private Map<String, String> workAddr = new HashMap<String, String>();
109
110    private String firstName;
111    private String lastName;
112    private String middleName;
113    private String prefix;
114    private String suffix;
115
116    private String emailHome;
117    private String emailWork;
118
119    private String organization;
120    private String organizationUnit;
121
122    private String photoMimeType;
123    private String photoBinval;
124
125    /**
126     * Such as DESC ROLE GEO etc.. see XEP-0054
127     */
128    private Map<String, String> otherSimpleFields = new HashMap<String, String>();
129
130    // fields that, as they are should not be escaped before forwarding to the server
131    private Map<String, String> otherUnescapableFields = new HashMap<String, String>();
132
133    public VCard() {
134        super(ELEMENT, NAMESPACE);
135    }
136
137    /**
138     * Set generic VCard field.
139     *
140     * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ,
141     *              GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC.
142     */
143    public String getField(String field) {
144        return otherSimpleFields.get(field);
145    }
146
147    /**
148     * Set generic VCard field.
149     *
150     * @param value value of field
151     * @param field field to set. See {@link #getField(String)}
152     * @see #getField(String)
153     */
154    public void setField(String field, String value) {
155        setField(field, value, false);
156    }
157
158    /**
159     * Set generic, unescapable VCard field. If unescabale is set to true, XML maybe a part of the
160     * value.
161     *
162     * @param value         value of field
163     * @param field         field to set. See {@link #getField(String)}
164     * @param isUnescapable True if the value should not be escaped, and false if it should.
165     */
166    public void setField(String field, String value, boolean isUnescapable) {
167        if (!isUnescapable) {
168            otherSimpleFields.put(field, value);
169        }
170        else {
171            otherUnescapableFields.put(field, value);
172        }
173    }
174
175    public String getFirstName() {
176        return firstName;
177    }
178
179    public void setFirstName(String firstName) {
180        this.firstName = firstName;
181        // Update FN field
182        updateFN();
183    }
184
185    public String getLastName() {
186        return lastName;
187    }
188
189    public void setLastName(String lastName) {
190        this.lastName = lastName;
191        // Update FN field
192        updateFN();
193    }
194
195    public String getMiddleName() {
196        return middleName;
197    }
198
199    public void setMiddleName(String middleName) {
200        this.middleName = middleName;
201        // Update FN field
202        updateFN();
203    }
204
205    public String getPrefix() {
206        return prefix;
207    }
208
209    public void setPrefix(String prefix) {
210        this.prefix = prefix;
211        updateFN();
212    }
213
214    public String getSuffix() {
215        return suffix;
216    }
217
218    public void setSuffix(String suffix) {
219        this.suffix = suffix;
220        updateFN();
221    }
222
223    public String getNickName() {
224        return otherSimpleFields.get("NICKNAME");
225    }
226
227    public void setNickName(String nickName) {
228        otherSimpleFields.put("NICKNAME", nickName);
229    }
230
231    public String getEmailHome() {
232        return emailHome;
233    }
234
235    public void setEmailHome(String email) {
236        this.emailHome = email;
237    }
238
239    public String getEmailWork() {
240        return emailWork;
241    }
242
243    public void setEmailWork(String emailWork) {
244        this.emailWork = emailWork;
245    }
246
247    public String getJabberId() {
248        return otherSimpleFields.get("JABBERID");
249    }
250
251    public void setJabberId(String jabberId) {
252        otherSimpleFields.put("JABBERID", jabberId);
253    }
254
255    public String getOrganization() {
256        return organization;
257    }
258
259    public void setOrganization(String organization) {
260        this.organization = organization;
261    }
262
263    public String getOrganizationUnit() {
264        return organizationUnit;
265    }
266
267    public void setOrganizationUnit(String organizationUnit) {
268        this.organizationUnit = organizationUnit;
269    }
270
271    /**
272     * Get home address field.
273     *
274     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
275     *                  LOCALITY, REGION, PCODE, CTRY
276     */
277    public String getAddressFieldHome(String addrField) {
278        return homeAddr.get(addrField);
279    }
280
281    /**
282     * Set home address field.
283     *
284     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
285     *                  LOCALITY, REGION, PCODE, CTRY
286     */
287    public void setAddressFieldHome(String addrField, String value) {
288        homeAddr.put(addrField, value);
289    }
290
291    /**
292     * Get work address field.
293     *
294     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
295     *                  LOCALITY, REGION, PCODE, CTRY
296     */
297    public String getAddressFieldWork(String addrField) {
298        return workAddr.get(addrField);
299    }
300
301    /**
302     * Set work address field.
303     *
304     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
305     *                  LOCALITY, REGION, PCODE, CTRY
306     */
307    public void setAddressFieldWork(String addrField, String value) {
308        workAddr.put(addrField, value);
309    }
310
311
312    /**
313     * Set home phone number.
314     *
315     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
316     * @param phoneNum  phone number
317     */
318    public void setPhoneHome(String phoneType, String phoneNum) {
319        homePhones.put(phoneType, phoneNum);
320    }
321
322    /**
323     * Get home phone number.
324     *
325     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
326     */
327    public String getPhoneHome(String phoneType) {
328        return homePhones.get(phoneType);
329    }
330
331    /**
332     * Set work phone number.
333     *
334     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
335     * @param phoneNum  phone number
336     */
337    public void setPhoneWork(String phoneType, String phoneNum) {
338        workPhones.put(phoneType, phoneNum);
339    }
340
341    /**
342     * Get work phone number.
343     *
344     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
345     */
346    public String getPhoneWork(String phoneType) {
347        return workPhones.get(phoneType);
348    }
349
350    /**
351     * Set the avatar for the VCard by specifying the url to the image.
352     *
353     * @param avatarURL the url to the image(png,jpeg,gif,bmp)
354     */
355    public void setAvatar(URL avatarURL) {
356        byte[] bytes = new byte[0];
357        try {
358            bytes = getBytes(avatarURL);
359        }
360        catch (IOException e) {
361            LOGGER.log(Level.SEVERE, "Error getting bytes from URL: " + avatarURL, e);
362        }
363
364        setAvatar(bytes);
365    }
366
367    /**
368     * Removes the avatar from the vCard.
369     *
370     *  This is done by setting the PHOTO value to the empty string as defined in XEP-0153
371     */
372    public void removeAvatar() {
373        // Remove avatar (if any)
374        photoBinval = null;
375        photoMimeType = null;
376    }
377
378    /**
379     * Specify the bytes of the JPEG for the avatar to use.
380     * If bytes is null, then the avatar will be removed.
381     * 'image/jpeg' will be used as MIME type.
382     *
383     * @param bytes the bytes of the avatar, or null to remove the avatar data
384     */
385    public void setAvatar(byte[] bytes) {
386        setAvatar(bytes, DEFAULT_MIME_TYPE);
387    }
388
389    /**
390     * Specify the bytes for the avatar to use as well as the mime type.
391     *
392     * @param bytes the bytes of the avatar.
393     * @param mimeType the mime type of the avatar.
394     */
395    public void setAvatar(byte[] bytes, String mimeType) {
396        // If bytes is null, remove the avatar
397        if (bytes == null) {
398            removeAvatar();
399            return;
400        }
401
402        // Otherwise, add to mappings.
403        String encodedImage = Base64.encodeToString(bytes);
404
405        setAvatar(encodedImage, mimeType);
406    }
407
408    /**
409     * Specify the Avatar used for this vCard.
410     *
411     * @param encodedImage the Base64 encoded image as String
412     * @param mimeType the MIME type of the image
413     */
414    public void setAvatar(String encodedImage, String mimeType) {
415        photoBinval = encodedImage;
416        photoMimeType = mimeType;
417    }
418
419    /**
420     * Set the encoded avatar string. This is used by the provider.
421     *
422     * @param encodedAvatar the encoded avatar string.
423     * @deprecated Use {@link #setAvatar(String, String)} instead.
424     */
425    @Deprecated
426    public void setEncodedImage(String encodedAvatar) {
427        setAvatar(encodedAvatar, DEFAULT_MIME_TYPE);
428    }
429
430    /**
431     * Return the byte representation of the avatar(if one exists), otherwise returns null if
432     * no avatar could be found.
433     * <b>Example 1</b>
434     * <pre>
435     * // Load Avatar from VCard
436     * byte[] avatarBytes = vCard.getAvatar();
437     * <p/>
438     * // To create an ImageIcon for Swing applications
439     * ImageIcon icon = new ImageIcon(avatar);
440     * <p/>
441     * // To create just an image object from the bytes
442     * ByteArrayInputStream bais = new ByteArrayInputStream(avatar);
443     * try {
444     *   Image image = ImageIO.read(bais);
445     *  }
446     *  catch (IOException e) {
447     *    e.printStackTrace();
448     * }
449     * </pre>
450     *
451     * @return byte representation of avatar.
452     */
453    public byte[] getAvatar() {
454        if (photoBinval == null) {
455            return null;
456        }
457        return Base64.decode(photoBinval);
458    }
459
460    /**
461     * Returns the MIME Type of the avatar or null if none is set.
462     *
463     * @return the MIME Type of the avatar or null
464     */
465    public String getAvatarMimeType() {
466        return photoMimeType;
467    }
468
469    /**
470     * Common code for getting the bytes of a url.
471     *
472     * @param url the url to read.
473     */
474    public static byte[] getBytes(URL url) throws IOException {
475        final String path = url.getPath();
476        final File file = new File(path);
477        if (file.exists()) {
478            return getFileBytes(file);
479        }
480
481        return null;
482    }
483
484    private static byte[] getFileBytes(File file) throws IOException {
485        BufferedInputStream bis = null;
486        try {
487            bis = new BufferedInputStream(new FileInputStream(file));
488            int bytes = (int) file.length();
489            byte[] buffer = new byte[bytes];
490            int readBytes = bis.read(buffer);
491            if (readBytes != buffer.length) {
492                throw new IOException("Entire file not read");
493            }
494            return buffer;
495        }
496        finally {
497            if (bis != null) {
498                bis.close();
499            }
500        }
501    }
502
503    /**
504     * Returns the SHA-1 Hash of the Avatar image.
505     *
506     * @return the SHA-1 Hash of the Avatar image.
507     */
508    public String getAvatarHash() {
509        byte[] bytes = getAvatar();
510        if (bytes == null) {
511            return null;
512        }
513
514        MessageDigest digest;
515        try {
516            digest = MessageDigest.getInstance("SHA-1");
517        }
518        catch (NoSuchAlgorithmException e) {
519            LOGGER.log(Level.SEVERE, "Failed to get message digest", e);
520            return null;
521        }
522
523        digest.update(bytes);
524        return StringUtils.encodeHex(digest.digest());
525    }
526
527    private void updateFN() {
528        StringBuilder sb = new StringBuilder();
529        if (firstName != null) {
530            sb.append(StringUtils.escapeForXml(firstName)).append(' ');
531        }
532        if (middleName != null) {
533            sb.append(StringUtils.escapeForXml(middleName)).append(' ');
534        }
535        if (lastName != null) {
536            sb.append(StringUtils.escapeForXml(lastName));
537        }
538        setField("FN", sb.toString());
539    }
540
541    /**
542     * Save this vCard for the user connected by 'connection'. XMPPConnection should be authenticated
543     * and not anonymous.
544     *
545     * @param connection the XMPPConnection to use.
546     * @throws XMPPErrorException thrown if there was an issue setting the VCard in the server.
547     * @throws NoResponseException if there was no response from the server.
548     * @throws NotConnectedException 
549     * @throws InterruptedException 
550     * @deprecated use {@link VCardManager#saveVCard(VCard)} instead.
551     */
552    @Deprecated
553    public void save(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
554        VCardManager.getInstanceFor(connection).saveVCard(this);
555    }
556
557    /**
558     * Load VCard information for a connected user. XMPPConnection should be authenticated
559     * and not anonymous.
560     * @throws XMPPErrorException 
561     * @throws NoResponseException 
562     * @throws NotConnectedException 
563     * @throws InterruptedException 
564     * @deprecated use {@link VCardManager#loadVCard()} instead.
565     */
566    @Deprecated
567    public void load(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
568        load(connection, null);
569    }
570
571    /**
572     * Load VCard information for a given user. XMPPConnection should be authenticated and not anonymous.
573     * @throws XMPPErrorException 
574     * @throws NoResponseException if there was no response from the server.
575     * @throws NotConnectedException 
576     * @throws InterruptedException 
577     * @deprecated use {@link VCardManager#loadVCard(EntityBareJid)} instead.
578     */
579    @Deprecated
580    public void load(XMPPConnection connection, EntityBareJid user) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
581        VCard result = VCardManager.getInstanceFor(connection).loadVCard(user);
582        copyFieldsFrom(result);
583    }
584
585    @Override
586    protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
587        if (!hasContent()) {
588            xml.setEmptyElement();
589            return xml;
590        }
591        xml.rightAngleBracket();
592        if (hasNameField()) {
593            xml.openElement("N");
594            xml.optElement("FAMILY", lastName);
595            xml.optElement("GIVEN", firstName);
596            xml.optElement("MIDDLE", middleName);
597            xml.optElement("PREFIX", prefix);
598            xml.optElement("SUFFIX", suffix);
599            xml.closeElement("N");
600        }
601        if (hasOrganizationFields()) {
602            xml.openElement("ORG");
603            xml.optElement("ORGNAME", organization);
604            xml.optElement("ORGUNIT", organizationUnit);
605            xml.closeElement("ORG");
606        }
607        for (Entry<String, String> entry : otherSimpleFields.entrySet()) {
608            xml.optElement(entry.getKey(), entry.getValue());
609        }
610        for (Entry<String, String> entry : otherUnescapableFields.entrySet()) {
611            final String value = entry.getValue();
612            if (value == null) {
613                continue;
614            }
615            xml.openElement(entry.getKey());
616            xml.append(value);
617            xml.closeElement(entry.getKey());
618        }
619        if (photoBinval != null) {
620            xml.openElement("PHOTO");
621            xml.escapedElement("BINVAL", photoBinval);
622            xml.element("TYPE", photoMimeType);
623            xml.closeElement("PHOTO");
624        }
625        if (emailWork != null) {
626            xml.openElement("EMAIL");
627            xml.emptyElement("WORK");
628            xml.emptyElement("INTERNET");
629            xml.emptyElement("PREF");
630            xml.element("USERID", emailWork);
631            xml.closeElement("EMAIL");
632        }
633        if (emailHome != null) {
634            xml.openElement("EMAIL");
635            xml.emptyElement("HOME");
636            xml.emptyElement("INTERNET");
637            xml.emptyElement("PREF");
638            xml.element("USERID", emailHome);
639            xml.closeElement("EMAIL");
640        }
641        for (Entry<String, String> phone : workPhones.entrySet()) {
642            final String number = phone.getValue();
643            if (number == null) {
644                continue;
645            }
646            xml.openElement("TEL");
647            xml.emptyElement("WORK");
648            xml.emptyElement(phone.getKey());
649            xml.element("NUMBER", number);
650            xml.closeElement("TEL");
651        }
652        for (Entry<String, String> phone : homePhones.entrySet()) {
653            final String number = phone.getValue();
654            if (number == null) {
655                continue;
656            }
657            xml.openElement("TEL");
658            xml.emptyElement("HOME");
659            xml.emptyElement(phone.getKey());
660            xml.element("NUMBER", number);
661            xml.closeElement("TEL");
662        }
663        if (!workAddr.isEmpty()) {
664            xml.openElement("ADR");
665            xml.emptyElement("WORK");
666            for (Entry<String, String> entry : workAddr.entrySet()) {
667                final String value = entry.getValue();
668                if (value == null) {
669                    continue;
670                }
671                xml.element(entry.getKey(), value);
672            }
673            xml.closeElement("ADR");
674        }
675        if (!homeAddr.isEmpty()) {
676            xml.openElement("ADR");
677            xml.emptyElement("HOME");
678            for (Entry<String, String> entry : homeAddr.entrySet()) {
679                final String value = entry.getValue();
680                if (value == null) {
681                    continue;
682                }
683                xml.element(entry.getKey(), value);
684            }
685            xml.closeElement("ADR");
686        }
687        return xml;
688    }
689
690    private void copyFieldsFrom(VCard from) {
691        Field[] fields = VCard.class.getDeclaredFields();
692        for (Field field : fields) {
693            if (field.getDeclaringClass() == VCard.class &&
694                    !Modifier.isFinal(field.getModifiers())) {
695                try {
696                    field.setAccessible(true);
697                    field.set(this, field.get(from));
698                }
699                catch (IllegalAccessException e) {
700                    throw new RuntimeException("This cannot happen:" + field, e);
701                }
702            }
703        }
704    }
705
706    private boolean hasContent() {
707        // noinspection OverlyComplexBooleanExpression
708        return hasNameField()
709                || hasOrganizationFields()
710                || emailHome != null
711                || emailWork != null
712                || otherSimpleFields.size() > 0
713                || otherUnescapableFields.size() > 0
714                || homeAddr.size() > 0
715                || homePhones.size() > 0
716                || workAddr.size() > 0
717                || workPhones.size() > 0
718                || photoBinval != null
719                ;
720    }
721
722    private boolean hasNameField() {
723        return firstName != null || lastName != null || middleName != null
724                || prefix != null || suffix != null;
725    }
726
727    private boolean hasOrganizationFields() {
728        return organization != null || organizationUnit != null;
729    }
730
731    // Used in tests:
732
733    @Override
734    public boolean equals(Object o) {
735        if (this == o) return true;
736        if (o == null || getClass() != o.getClass()) return false;
737
738        final VCard vCard = (VCard) o;
739
740        if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) {
741            return false;
742        }
743        if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) {
744            return false;
745        }
746        if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) {
747            return false;
748        }
749        if (!homeAddr.equals(vCard.homeAddr)) {
750            return false;
751        }
752        if (!homePhones.equals(vCard.homePhones)) {
753            return false;
754        }
755        if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) {
756            return false;
757        }
758        if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) {
759            return false;
760        }
761        if (organization != null ?
762                !organization.equals(vCard.organization) : vCard.organization != null) {
763            return false;
764        }
765        if (organizationUnit != null ?
766                !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) {
767            return false;
768        }
769        if (!otherSimpleFields.equals(vCard.otherSimpleFields)) {
770            return false;
771        }
772        if (!workAddr.equals(vCard.workAddr)) {
773            return false;
774        }
775        if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) {
776            return false;
777        }
778
779        return workPhones.equals(vCard.workPhones);
780    }
781
782    @Override
783    public int hashCode() {
784        int result;
785        result = homePhones.hashCode();
786        result = 29 * result + workPhones.hashCode();
787        result = 29 * result + homeAddr.hashCode();
788        result = 29 * result + workAddr.hashCode();
789        result = 29 * result + (firstName != null ? firstName.hashCode() : 0);
790        result = 29 * result + (lastName != null ? lastName.hashCode() : 0);
791        result = 29 * result + (middleName != null ? middleName.hashCode() : 0);
792        result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0);
793        result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0);
794        result = 29 * result + (organization != null ? organization.hashCode() : 0);
795        result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0);
796        result = 29 * result + otherSimpleFields.hashCode();
797        result = 29 * result + (photoBinval != null ? photoBinval.hashCode() : 0);
798        return result;
799    }
800
801}
802