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