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