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