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