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.smackx.vcardtemp.VCardManager; 042 043/** 044 * A VCard class for use with the 045 * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p> 046 * <p/> 047 * You should refer to the 048 * <a href="http://www.xmpp.org/extensions/jep-0054.html" target="_blank">XEP-54 documentation</a>.<p> 049 * <p/> 050 * Please note that this class is incomplete but it does provide the most commonly found 051 * information in vCards. Also remember that VCard transfer is not a standard, and the protocol 052 * may change or be replaced.<p> 053 * <p/> 054 * <b>Usage:</b> 055 * <pre> 056 * <p/> 057 * // To save VCard: 058 * <p/> 059 * VCard vCard = new VCard(); 060 * vCard.setFirstName("kir"); 061 * vCard.setLastName("max"); 062 * vCard.setEmailHome("foo@fee.bar"); 063 * vCard.setJabberId("jabber@id.org"); 064 * vCard.setOrganization("Jetbrains, s.r.o"); 065 * vCard.setNickName("KIR"); 066 * <p/> 067 * vCard.setField("TITLE", "Mr"); 068 * vCard.setAddressFieldHome("STREET", "Some street"); 069 * vCard.setAddressFieldWork("CTRY", "US"); 070 * vCard.setPhoneWork("FAX", "3443233"); 071 * <p/> 072 * vCard.save(connection); 073 * <p/> 074 * // To load VCard: 075 * <p/> 076 * VCard vCard = new VCard(); 077 * vCard.load(conn); // load own VCard 078 * vCard.load(conn, "joe@foo.bar"); // load someone's VCard 079 * </pre> 080 * 081 * @author Kirill Maximov (kir@maxkir.com) 082 */ 083public class VCard extends IQ { 084 private static final Logger LOGGER = Logger.getLogger(VCard.class.getName()); 085 086 private static final String DEFAULT_MIME_TYPE = "image/jpeg"; 087 088 /** 089 * Phone types: 090 * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF? 091 */ 092 private Map<String, String> homePhones = new HashMap<String, String>(); 093 private Map<String, String> workPhones = new HashMap<String, String>(); 094 095 /** 096 * Address types: 097 * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?, 098 * REGION?, PCODE?, CTRY? 099 */ 100 private Map<String, String> homeAddr = new HashMap<String, String>(); 101 private Map<String, String> workAddr = new HashMap<String, String>(); 102 103 private String firstName; 104 private String lastName; 105 private String middleName; 106 107 private String emailHome; 108 private String emailWork; 109 110 private String organization; 111 private String organizationUnit; 112 113 private String photoMimeType; 114 private String photoBinval; 115 116 /** 117 * Such as DESC ROLE GEO etc.. see XEP-0054 118 */ 119 private Map<String, String> otherSimpleFields = new HashMap<String, String>(); 120 121 // fields that, as they are should not be escaped before forwarding to the server 122 private Map<String, String> otherUnescapableFields = new HashMap<String, String>(); 123 124 public VCard() { 125 } 126 127 /** 128 * Set generic VCard field. 129 * 130 * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ, 131 * GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC. 132 */ 133 public String getField(String field) { 134 return otherSimpleFields.get(field); 135 } 136 137 /** 138 * Set generic VCard field. 139 * 140 * @param value value of field 141 * @param field field to set. See {@link #getField(String)} 142 * @see #getField(String) 143 */ 144 public void setField(String field, String value) { 145 setField(field, value, false); 146 } 147 148 /** 149 * Set generic, unescapable VCard field. If unescabale is set to true, XML maybe a part of the 150 * value. 151 * 152 * @param value value of field 153 * @param field field to set. See {@link #getField(String)} 154 * @param isUnescapable True if the value should not be escaped, and false if it should. 155 */ 156 public void setField(String field, String value, boolean isUnescapable) { 157 if (!isUnescapable) { 158 otherSimpleFields.put(field, value); 159 } 160 else { 161 otherUnescapableFields.put(field, value); 162 } 163 } 164 165 public String getFirstName() { 166 return firstName; 167 } 168 169 public void setFirstName(String firstName) { 170 this.firstName = firstName; 171 // Update FN field 172 updateFN(); 173 } 174 175 public String getLastName() { 176 return lastName; 177 } 178 179 public void setLastName(String lastName) { 180 this.lastName = lastName; 181 // Update FN field 182 updateFN(); 183 } 184 185 public String getMiddleName() { 186 return middleName; 187 } 188 189 public void setMiddleName(String middleName) { 190 this.middleName = middleName; 191 // Update FN field 192 updateFN(); 193 } 194 195 public String getNickName() { 196 return otherSimpleFields.get("NICKNAME"); 197 } 198 199 public void setNickName(String nickName) { 200 otherSimpleFields.put("NICKNAME", nickName); 201 } 202 203 public String getEmailHome() { 204 return emailHome; 205 } 206 207 public void setEmailHome(String email) { 208 this.emailHome = email; 209 } 210 211 public String getEmailWork() { 212 return emailWork; 213 } 214 215 public void setEmailWork(String emailWork) { 216 this.emailWork = emailWork; 217 } 218 219 public String getJabberId() { 220 return otherSimpleFields.get("JABBERID"); 221 } 222 223 public void setJabberId(String jabberId) { 224 otherSimpleFields.put("JABBERID", jabberId); 225 } 226 227 public String getOrganization() { 228 return organization; 229 } 230 231 public void setOrganization(String organization) { 232 this.organization = organization; 233 } 234 235 public String getOrganizationUnit() { 236 return organizationUnit; 237 } 238 239 public void setOrganizationUnit(String organizationUnit) { 240 this.organizationUnit = organizationUnit; 241 } 242 243 /** 244 * Get home address field 245 * 246 * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, 247 * LOCALITY, REGION, PCODE, CTRY 248 */ 249 public String getAddressFieldHome(String addrField) { 250 return homeAddr.get(addrField); 251 } 252 253 /** 254 * Set home address field 255 * 256 * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, 257 * LOCALITY, REGION, PCODE, CTRY 258 */ 259 public void setAddressFieldHome(String addrField, String value) { 260 homeAddr.put(addrField, value); 261 } 262 263 /** 264 * Get work address field 265 * 266 * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, 267 * LOCALITY, REGION, PCODE, CTRY 268 */ 269 public String getAddressFieldWork(String addrField) { 270 return workAddr.get(addrField); 271 } 272 273 /** 274 * Set work address field 275 * 276 * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, 277 * LOCALITY, REGION, PCODE, CTRY 278 */ 279 public void setAddressFieldWork(String addrField, String value) { 280 workAddr.put(addrField, value); 281 } 282 283 284 /** 285 * Set home phone number 286 * 287 * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF 288 * @param phoneNum phone number 289 */ 290 public void setPhoneHome(String phoneType, String phoneNum) { 291 homePhones.put(phoneType, phoneNum); 292 } 293 294 /** 295 * Get home phone number 296 * 297 * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF 298 */ 299 public String getPhoneHome(String phoneType) { 300 return homePhones.get(phoneType); 301 } 302 303 /** 304 * Set work phone number 305 * 306 * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF 307 * @param phoneNum phone number 308 */ 309 public void setPhoneWork(String phoneType, String phoneNum) { 310 workPhones.put(phoneType, phoneNum); 311 } 312 313 /** 314 * Get work phone number 315 * 316 * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF 317 */ 318 public String getPhoneWork(String phoneType) { 319 return workPhones.get(phoneType); 320 } 321 322 /** 323 * Set the avatar for the VCard by specifying the url to the image. 324 * 325 * @param avatarURL the url to the image(png,jpeg,gif,bmp) 326 */ 327 public void setAvatar(URL avatarURL) { 328 byte[] bytes = new byte[0]; 329 try { 330 bytes = getBytes(avatarURL); 331 } 332 catch (IOException e) { 333 LOGGER.log(Level.SEVERE, "Error getting bytes from URL: " + avatarURL, e); 334 } 335 336 setAvatar(bytes); 337 } 338 339 /** 340 * Removes the avatar from the vCard 341 * 342 * This is done by setting the PHOTO value to the empty string as defined in XEP-0153 343 */ 344 public void removeAvatar() { 345 // Remove avatar (if any) 346 photoBinval = null; 347 photoMimeType = null; 348 } 349 350 /** 351 * Specify the bytes of the JPEG for the avatar to use. 352 * If bytes is null, then the avatar will be removed. 353 * 'image/jpeg' will be used as MIME type. 354 * 355 * @param bytes the bytes of the avatar, or null to remove the avatar data 356 */ 357 public void setAvatar(byte[] bytes) { 358 setAvatar(bytes, DEFAULT_MIME_TYPE); 359 } 360 361 /** 362 * Specify the bytes for the avatar to use as well as the mime type. 363 * 364 * @param bytes the bytes of the avatar. 365 * @param mimeType the mime type of the avatar. 366 */ 367 public void setAvatar(byte[] bytes, String mimeType) { 368 // If bytes is null, remove the avatar 369 if (bytes == null) { 370 removeAvatar(); 371 return; 372 } 373 374 // Otherwise, add to mappings. 375 String encodedImage = StringUtils.encodeBase64(bytes); 376 377 setAvatar(encodedImage, mimeType); 378 } 379 380 /** 381 * Specify the Avatar used for this vCard. 382 * 383 * @param encodedImage the Base64 encoded image as String 384 * @param mimeType the MIME type of the image 385 */ 386 public void setAvatar(String encodedImage, String mimeType) { 387 photoBinval = encodedImage; 388 photoMimeType = mimeType; 389 } 390 391 /** 392 * Set the encoded avatar string. This is used by the provider. 393 * 394 * @param encodedAvatar the encoded avatar string. 395 * @deprecated Use {@link #setAvatar(String, String)} instead. 396 */ 397 public void setEncodedImage(String encodedAvatar) { 398 setAvatar(encodedAvatar, DEFAULT_MIME_TYPE); 399 } 400 401 /** 402 * Return the byte representation of the avatar(if one exists), otherwise returns null if 403 * no avatar could be found. 404 * <b>Example 1</b> 405 * <pre> 406 * // Load Avatar from VCard 407 * byte[] avatarBytes = vCard.getAvatar(); 408 * <p/> 409 * // To create an ImageIcon for Swing applications 410 * ImageIcon icon = new ImageIcon(avatar); 411 * <p/> 412 * // To create just an image object from the bytes 413 * ByteArrayInputStream bais = new ByteArrayInputStream(avatar); 414 * try { 415 * Image image = ImageIO.read(bais); 416 * } 417 * catch (IOException e) { 418 * e.printStackTrace(); 419 * } 420 * </pre> 421 * 422 * @return byte representation of avatar. 423 */ 424 public byte[] getAvatar() { 425 if (photoBinval == null) { 426 return null; 427 } 428 return StringUtils.decodeBase64(photoBinval); 429 } 430 431 /** 432 * Returns the MIME Type of the avatar or null if none is set 433 * 434 * @return the MIME Type of the avatar or null 435 */ 436 public String getAvatarMimeType() { 437 return photoMimeType; 438 } 439 440 /** 441 * Common code for getting the bytes of a url. 442 * 443 * @param url the url to read. 444 */ 445 public static byte[] getBytes(URL url) throws IOException { 446 final String path = url.getPath(); 447 final File file = new File(path); 448 if (file.exists()) { 449 return getFileBytes(file); 450 } 451 452 return null; 453 } 454 455 private static byte[] getFileBytes(File file) throws IOException { 456 BufferedInputStream bis = null; 457 try { 458 bis = new BufferedInputStream(new FileInputStream(file)); 459 int bytes = (int) file.length(); 460 byte[] buffer = new byte[bytes]; 461 int readBytes = bis.read(buffer); 462 if (readBytes != buffer.length) { 463 throw new IOException("Entire file not read"); 464 } 465 return buffer; 466 } 467 finally { 468 if (bis != null) { 469 bis.close(); 470 } 471 } 472 } 473 474 /** 475 * Returns the SHA-1 Hash of the Avatar image. 476 * 477 * @return the SHA-1 Hash of the Avatar image. 478 */ 479 public String getAvatarHash() { 480 byte[] bytes = getAvatar(); 481 if (bytes == null) { 482 return null; 483 } 484 485 MessageDigest digest; 486 try { 487 digest = MessageDigest.getInstance("SHA-1"); 488 } 489 catch (NoSuchAlgorithmException e) { 490 LOGGER.log(Level.SEVERE, "Failed to get message digest", e); 491 return null; 492 } 493 494 digest.update(bytes); 495 return StringUtils.encodeHex(digest.digest()); 496 } 497 498 private void updateFN() { 499 StringBuilder sb = new StringBuilder(); 500 if (firstName != null) { 501 sb.append(StringUtils.escapeForXML(firstName)).append(' '); 502 } 503 if (middleName != null) { 504 sb.append(StringUtils.escapeForXML(middleName)).append(' '); 505 } 506 if (lastName != null) { 507 sb.append(StringUtils.escapeForXML(lastName)); 508 } 509 setField("FN", sb.toString()); 510 } 511 512 /** 513 * Save this vCard for the user connected by 'connection'. XMPPConnection should be authenticated 514 * and not anonymous.<p> 515 * <p/> 516 * NOTE: the method is asynchronous and does not wait for the returned value. 517 * 518 * @param connection the XMPPConnection to use. 519 * @throws XMPPErrorException thrown if there was an issue setting the VCard in the server. 520 * @throws NoResponseException if there was no response from the server. 521 * @throws NotConnectedException 522 */ 523 public void save(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException { 524 checkAuthenticated(connection, true); 525 526 setType(IQ.Type.SET); 527 setFrom(connection.getUser()); 528 connection.createPacketCollectorAndSend(this).nextResultOrThrow(); 529 } 530 531 /** 532 * Load VCard information for a connected user. XMPPConnection should be authenticated 533 * and not anonymous. 534 * @throws XMPPErrorException 535 * @throws NoResponseException 536 * @throws NotConnectedException 537 */ 538 public void load(XMPPConnection connection) throws NoResponseException, XMPPErrorException, NotConnectedException { 539 checkAuthenticated(connection, true); 540 541 setFrom(connection.getUser()); 542 doLoad(connection, connection.getUser()); 543 } 544 545 /** 546 * Load VCard information for a given user. XMPPConnection should be authenticated and not anonymous. 547 * @throws XMPPErrorException 548 * @throws NoResponseException if there was no response from the server. 549 * @throws NotConnectedException 550 */ 551 public void load(XMPPConnection connection, String user) throws NoResponseException, XMPPErrorException, NotConnectedException { 552 checkAuthenticated(connection, false); 553 554 setTo(user); 555 doLoad(connection, user); 556 } 557 558 private void doLoad(XMPPConnection connection, String user) throws NoResponseException, XMPPErrorException, NotConnectedException { 559 setType(Type.GET); 560 VCard result = (VCard) connection.createPacketCollectorAndSend(this).nextResultOrThrow(); 561 copyFieldsFrom(result); 562 } 563 564 public String getChildElementXML() { 565 StringBuilder sb = new StringBuilder(); 566 new VCardWriter(sb).write(); 567 return sb.toString(); 568 } 569 570 private void copyFieldsFrom(VCard from) { 571 Field[] fields = VCard.class.getDeclaredFields(); 572 for (Field field : fields) { 573 if (field.getDeclaringClass() == VCard.class && 574 !Modifier.isFinal(field.getModifiers())) { 575 try { 576 field.setAccessible(true); 577 field.set(this, field.get(from)); 578 } 579 catch (IllegalAccessException e) { 580 throw new RuntimeException("This cannot happen:" + field, e); 581 } 582 } 583 } 584 } 585 586 private void checkAuthenticated(XMPPConnection connection, boolean checkForAnonymous) { 587 if (connection == null) { 588 throw new IllegalArgumentException("No connection was provided"); 589 } 590 if (!connection.isAuthenticated()) { 591 throw new IllegalArgumentException("XMPPConnection is not authenticated"); 592 } 593 if (checkForAnonymous && connection.isAnonymous()) { 594 throw new IllegalArgumentException("XMPPConnection cannot be anonymous"); 595 } 596 } 597 598 private boolean hasContent() { 599 //noinspection OverlyComplexBooleanExpression 600 return hasNameField() 601 || hasOrganizationFields() 602 || emailHome != null 603 || emailWork != null 604 || otherSimpleFields.size() > 0 605 || otherUnescapableFields.size() > 0 606 || homeAddr.size() > 0 607 || homePhones.size() > 0 608 || workAddr.size() > 0 609 || workPhones.size() > 0 610 || photoBinval != null 611 ; 612 } 613 614 private boolean hasNameField() { 615 return firstName != null || lastName != null || middleName != null; 616 } 617 618 private boolean hasOrganizationFields() { 619 return organization != null || organizationUnit != null; 620 } 621 622 // Used in tests: 623 624 public boolean equals(Object o) { 625 if (this == o) return true; 626 if (o == null || getClass() != o.getClass()) return false; 627 628 final VCard vCard = (VCard) o; 629 630 if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) { 631 return false; 632 } 633 if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) { 634 return false; 635 } 636 if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) { 637 return false; 638 } 639 if (!homeAddr.equals(vCard.homeAddr)) { 640 return false; 641 } 642 if (!homePhones.equals(vCard.homePhones)) { 643 return false; 644 } 645 if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) { 646 return false; 647 } 648 if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) { 649 return false; 650 } 651 if (organization != null ? 652 !organization.equals(vCard.organization) : vCard.organization != null) { 653 return false; 654 } 655 if (organizationUnit != null ? 656 !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) { 657 return false; 658 } 659 if (!otherSimpleFields.equals(vCard.otherSimpleFields)) { 660 return false; 661 } 662 if (!workAddr.equals(vCard.workAddr)) { 663 return false; 664 } 665 if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) { 666 return false; 667 } 668 669 return workPhones.equals(vCard.workPhones); 670 } 671 672 public int hashCode() { 673 int result; 674 result = homePhones.hashCode(); 675 result = 29 * result + workPhones.hashCode(); 676 result = 29 * result + homeAddr.hashCode(); 677 result = 29 * result + workAddr.hashCode(); 678 result = 29 * result + (firstName != null ? firstName.hashCode() : 0); 679 result = 29 * result + (lastName != null ? lastName.hashCode() : 0); 680 result = 29 * result + (middleName != null ? middleName.hashCode() : 0); 681 result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0); 682 result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0); 683 result = 29 * result + (organization != null ? organization.hashCode() : 0); 684 result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0); 685 result = 29 * result + otherSimpleFields.hashCode(); 686 result = 29 * result + (photoBinval != null ? photoBinval.hashCode() : 0); 687 return result; 688 } 689 690 public String toString() { 691 return getChildElementXML(); 692 } 693 694 //============================================================== 695 696 private class VCardWriter { 697 698 private final StringBuilder sb; 699 700 VCardWriter(StringBuilder sb) { 701 this.sb = sb; 702 } 703 704 public void write() { 705 appendTag(VCardManager.ELEMENT, "xmlns", VCardManager.NAMESPACE, hasContent(), new ContentBuilder() { 706 public void addTagContent() { 707 buildActualContent(); 708 } 709 }); 710 } 711 712 private void buildActualContent() { 713 if (hasNameField()) { 714 appendN(); 715 } 716 717 appendOrganization(); 718 appendGenericFields(); 719 appendPhoto(); 720 721 appendEmail(emailWork, "WORK"); 722 appendEmail(emailHome, "HOME"); 723 724 appendPhones(workPhones, "WORK"); 725 appendPhones(homePhones, "HOME"); 726 727 appendAddress(workAddr, "WORK"); 728 appendAddress(homeAddr, "HOME"); 729 } 730 731 private void appendPhoto() { 732 if (photoBinval == null) 733 return; 734 735 appendTag("PHOTO", true, new ContentBuilder() { 736 public void addTagContent() { 737 appendTag("BINVAL", photoBinval); // No need to escape photoBinval, as it's already Base64 encoded 738 appendTag("TYPE", StringUtils.escapeForXML(photoMimeType)); 739 } 740 }); 741 } 742 private void appendEmail(final String email, final String type) { 743 if (email != null) { 744 appendTag("EMAIL", true, new ContentBuilder() { 745 public void addTagContent() { 746 appendEmptyTag(type); 747 appendEmptyTag("INTERNET"); 748 appendEmptyTag("PREF"); 749 appendTag("USERID", StringUtils.escapeForXML(email)); 750 } 751 }); 752 } 753 } 754 755 private void appendPhones(Map<String, String> phones, final String code) { 756 for (final Map.Entry<String,String> entry : phones.entrySet()) { 757 appendTag("TEL", true, new ContentBuilder() { 758 public void addTagContent() { 759 appendEmptyTag(entry.getKey()); 760 appendEmptyTag(code); 761 appendTag("NUMBER", StringUtils.escapeForXML(entry.getValue())); 762 } 763 }); 764 } 765 } 766 767 private void appendAddress(final Map<String, String> addr, final String code) { 768 if (addr.size() > 0) { 769 appendTag("ADR", true, new ContentBuilder() { 770 public void addTagContent() { 771 appendEmptyTag(code); 772 773 for (final Entry<String, String> entry : addr.entrySet()) { 774 appendTag(entry.getKey(), StringUtils.escapeForXML(entry.getValue())); 775 } 776 } 777 }); 778 } 779 } 780 781 private void appendEmptyTag(Object tag) { 782 sb.append('<').append(tag).append("/>"); 783 } 784 785 private void appendGenericFields() { 786 for (Map.Entry<String, String> entry : otherSimpleFields.entrySet()) { 787 appendTag(entry.getKey().toString(), 788 StringUtils.escapeForXML(entry.getValue())); 789 } 790 791 for (Map.Entry<String, String> entry : otherUnescapableFields.entrySet()) { 792 appendTag(entry.getKey().toString(),entry.getValue()); 793 } 794 } 795 796 private void appendOrganization() { 797 if (hasOrganizationFields()) { 798 appendTag("ORG", true, new ContentBuilder() { 799 public void addTagContent() { 800 appendTag("ORGNAME", StringUtils.escapeForXML(organization)); 801 appendTag("ORGUNIT", StringUtils.escapeForXML(organizationUnit)); 802 } 803 }); 804 } 805 } 806 807 private void appendN() { 808 appendTag("N", true, new ContentBuilder() { 809 public void addTagContent() { 810 appendTag("FAMILY", StringUtils.escapeForXML(lastName)); 811 appendTag("GIVEN", StringUtils.escapeForXML(firstName)); 812 appendTag("MIDDLE", StringUtils.escapeForXML(middleName)); 813 } 814 }); 815 } 816 817 private void appendTag(String tag, String attr, String attrValue, boolean hasContent, 818 ContentBuilder builder) { 819 sb.append('<').append(tag); 820 if (attr != null) { 821 sb.append(' ').append(attr).append('=').append('\'').append(attrValue).append('\''); 822 } 823 824 if (hasContent) { 825 sb.append('>'); 826 builder.addTagContent(); 827 sb.append("</").append(tag).append(">\n"); 828 } 829 else { 830 sb.append("/>\n"); 831 } 832 } 833 834 private void appendTag(String tag, boolean hasContent, ContentBuilder builder) { 835 appendTag(tag, null, null, hasContent, builder); 836 } 837 838 private void appendTag(String tag, final CharSequence tagText) { 839 if (tagText == null) return; 840 final ContentBuilder contentBuilder = new ContentBuilder() { 841 public void addTagContent() { 842 sb.append(tagText.toString().trim()); 843 } 844 }; 845 appendTag(tag, true, contentBuilder); 846 } 847 848 } 849 850 //============================================================== 851 852 private interface ContentBuilder { 853 854 void addTagContent(); 855 } 856 857 //============================================================== 858} 859