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