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