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