001/** 002 * 003 * Copyright 2014-2023 Florian Schmaus 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 */ 017package org.jivesoftware.smack.util; 018 019import java.io.IOException; 020import java.io.Writer; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Date; 024import java.util.List; 025 026import org.jivesoftware.smack.packet.Element; 027import org.jivesoftware.smack.packet.NamedElement; 028import org.jivesoftware.smack.packet.XmlElement; 029import org.jivesoftware.smack.packet.XmlEnvironment; 030 031import org.jxmpp.jid.Jid; 032import org.jxmpp.util.XmppDateTime; 033 034public class XmlStringBuilder implements Appendable, CharSequence, Element { 035 public static final String RIGHT_ANGLE_BRACKET = Character.toString('>'); 036 037 private final LazyStringBuilder sb; 038 039 private final XmlEnvironment effectiveXmlEnvironment; 040 041 public XmlStringBuilder() { 042 sb = new LazyStringBuilder(); 043 effectiveXmlEnvironment = null; 044 } 045 046 public XmlStringBuilder(XmlElement pe) { 047 this(pe, null); 048 } 049 050 public XmlStringBuilder(NamedElement e) { 051 this(); 052 halfOpenElement(e.getElementName()); 053 } 054 055 public XmlStringBuilder(XmlElement element, XmlEnvironment enclosingXmlEnvironment) { 056 this(element.getElementName(), element.getNamespace(), element.getLanguage(), enclosingXmlEnvironment); 057 } 058 059 public XmlStringBuilder(String elementName, String xmlNs, String xmlLang, XmlEnvironment enclosingXmlEnvironment) { 060 sb = new LazyStringBuilder(); 061 halfOpenElement(elementName); 062 063 if (enclosingXmlEnvironment == null) { 064 xmlnsAttribute(xmlNs); 065 xmllangAttribute(xmlLang); 066 } else { 067 if (!enclosingXmlEnvironment.effectiveNamespaceEquals(xmlNs)) { 068 xmlnsAttribute(xmlNs); 069 } 070 if (!enclosingXmlEnvironment.effectiveLanguageEquals(xmlLang)) { 071 xmllangAttribute(xmlLang); 072 } 073 } 074 075 effectiveXmlEnvironment = XmlEnvironment.builder() 076 .withNamespace(xmlNs) 077 .withLanguage(xmlLang) 078 .withNext(enclosingXmlEnvironment) 079 .build(); 080 } 081 082 public XmlEnvironment getXmlEnvironment() { 083 return effectiveXmlEnvironment; 084 } 085 086 public XmlStringBuilder escapedElement(String name, String escapedContent) { 087 assert escapedContent != null; 088 openElement(name); 089 append(escapedContent); 090 closeElement(name); 091 return this; 092 } 093 094 /** 095 * Add a new element to this builder. 096 * 097 * @param name TODO javadoc me please 098 * @param content TODO javadoc me please 099 * @return the XmlStringBuilder 100 */ 101 public XmlStringBuilder element(String name, String content) { 102 if (content.isEmpty()) { 103 return emptyElement(name); 104 } 105 openElement(name); 106 escape(content); 107 closeElement(name); 108 return this; 109 } 110 111 /** 112 * Add a new element to this builder, with the {@link java.util.Date} instance as its content, 113 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}. 114 * 115 * @param name element name 116 * @param content content of element 117 * @return this XmlStringBuilder 118 */ 119 public XmlStringBuilder element(String name, Date content) { 120 assert content != null; 121 return element(name, XmppDateTime.formatXEP0082Date(content)); 122 } 123 124 /** 125 * Add a new element to this builder. 126 * 127 * @param name TODO javadoc me please 128 * @param content TODO javadoc me please 129 * @return the XmlStringBuilder 130 */ 131 public XmlStringBuilder element(String name, CharSequence content) { 132 return element(name, content.toString()); 133 } 134 135 public XmlStringBuilder element(String name, Enum<?> content) { 136 assert content != null; 137 element(name, content.toString()); 138 return this; 139 } 140 141 /** 142 * Deprecated. 143 * 144 * @param element deprecated. 145 * @return deprecated. 146 * @deprecated use {@link #append(Element)} instead. 147 */ 148 @Deprecated 149 // TODO: Remove in Smack 4.5. 150 public XmlStringBuilder element(Element element) { 151 assert element != null; 152 return append(element.toXML()); 153 } 154 155 public XmlStringBuilder optElement(String name, String content) { 156 if (content != null) { 157 element(name, content); 158 } 159 return this; 160 } 161 162 /** 163 * Add a new element to this builder, with the {@link java.util.Date} instance as its content, 164 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)} 165 * if {@link java.util.Date} instance is not <code>null</code>. 166 * 167 * @param name element name 168 * @param content content of element 169 * @return this XmlStringBuilder 170 */ 171 public XmlStringBuilder optElement(String name, Date content) { 172 if (content != null) { 173 element(name, content); 174 } 175 return this; 176 } 177 178 public XmlStringBuilder optElement(String name, CharSequence content) { 179 if (content != null) { 180 element(name, content.toString()); 181 } 182 return this; 183 } 184 185 public XmlStringBuilder optElement(Element element) { 186 if (element != null) { 187 append(element); 188 } 189 return this; 190 } 191 192 public XmlStringBuilder optElement(String name, Enum<?> content) { 193 if (content != null) { 194 element(name, content); 195 } 196 return this; 197 } 198 199 public XmlStringBuilder optElement(String name, Object object) { 200 if (object != null) { 201 element(name, object.toString()); 202 } 203 return this; 204 } 205 206 public XmlStringBuilder optIntElement(String name, int value) { 207 if (value >= 0) { 208 element(name, String.valueOf(value)); 209 } 210 return this; 211 } 212 213 public XmlStringBuilder halfOpenElement(String name) { 214 assert StringUtils.isNotEmpty(name); 215 sb.append('<').append(name); 216 return this; 217 } 218 219 public XmlStringBuilder halfOpenElement(NamedElement namedElement) { 220 return halfOpenElement(namedElement.getElementName()); 221 } 222 223 public XmlStringBuilder openElement(String name) { 224 halfOpenElement(name).rightAngleBracket(); 225 return this; 226 } 227 228 public XmlStringBuilder closeElement(String name) { 229 sb.append("</").append(name); 230 rightAngleBracket(); 231 return this; 232 } 233 234 public XmlStringBuilder closeElement(NamedElement e) { 235 closeElement(e.getElementName()); 236 return this; 237 } 238 239 public XmlStringBuilder closeEmptyElement() { 240 sb.append("/>"); 241 return this; 242 } 243 244 /** 245 * Add a right angle bracket '>'. 246 * 247 * @return a reference to this object. 248 */ 249 public XmlStringBuilder rightAngleBracket() { 250 sb.append(RIGHT_ANGLE_BRACKET); 251 return this; 252 } 253 254 /** 255 * Does nothing if value is null. 256 * 257 * @param name TODO javadoc me please 258 * @param value TODO javadoc me please 259 * @return the XmlStringBuilder 260 */ 261 public XmlStringBuilder attribute(String name, String value) { 262 assert value != null; 263 sb.append(' ').append(name).append("='"); 264 escapeAttributeValue(value); 265 sb.append('\''); 266 return this; 267 } 268 269 public XmlStringBuilder attribute(String name, boolean bool) { 270 return attribute(name, Boolean.toString(bool)); 271 } 272 273 /** 274 * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value, 275 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}. 276 * 277 * @param name name of attribute 278 * @param value value of attribute 279 * @return this XmlStringBuilder 280 */ 281 public XmlStringBuilder attribute(String name, Date value) { 282 assert value != null; 283 return attribute(name, XmppDateTime.formatXEP0082Date(value)); 284 } 285 286 public XmlStringBuilder attribute(String name, CharSequence value) { 287 return attribute(name, value.toString()); 288 } 289 290 public XmlStringBuilder attribute(String name, Enum<?> value) { 291 assert value != null; 292 attribute(name, value.toString()); 293 return this; 294 } 295 296 public <E extends Enum<?>> XmlStringBuilder attribute(String name, E value, E implicitDefault) { 297 if (value == null || value == implicitDefault) { 298 return this; 299 } 300 301 attribute(name, value.toString()); 302 return this; 303 } 304 305 public XmlStringBuilder attribute(String name, int value) { 306 assert name != null; 307 return attribute(name, String.valueOf(value)); 308 } 309 310 public XmlStringBuilder attribute(String name, long value) { 311 assert name != null; 312 return attribute(name, String.valueOf(value)); 313 } 314 315 public XmlStringBuilder jidAttribute(Jid jid) { 316 assert jid != null; 317 return attribute("jid", jid); 318 } 319 320 public XmlStringBuilder optJidAttribute(Jid jid) { 321 if (jid != null) { 322 attribute("jid", jid); 323 } 324 return this; 325 } 326 327 public XmlStringBuilder optAttribute(String name, String value) { 328 if (value != null) { 329 attribute(name, value); 330 } 331 return this; 332 } 333 334 public XmlStringBuilder optAttribute(String name, Long value) { 335 if (value != null) { 336 attribute(name, value); 337 } 338 return this; 339 } 340 341 /** 342 * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value, 343 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)} 344 * if {@link java.util.Date} instance is not <code>null</code>. 345 * 346 * @param name attribute name 347 * @param value value of this attribute 348 * @return this XmlStringBuilder 349 */ 350 public XmlStringBuilder optAttribute(String name, Date value) { 351 if (value != null) { 352 attribute(name, value); 353 } 354 return this; 355 } 356 357 public XmlStringBuilder optAttribute(String name, CharSequence value) { 358 if (value != null) { 359 attribute(name, value.toString()); 360 } 361 return this; 362 } 363 364 public XmlStringBuilder optAttribute(String name, Enum<?> value) { 365 if (value != null) { 366 attribute(name, value.toString()); 367 } 368 return this; 369 } 370 371 public XmlStringBuilder optAttribute(String name, Number number) { 372 if (number != null) { 373 attribute(name, number.toString()); 374 } 375 return this; 376 } 377 378 /** 379 * Same as {@link #optAttribute(String, CharSequence)}, but with a different method name. This method can be used if 380 * the provided attribute value argument type causes ambiguity in method overloading. For example if the type is a 381 * subclass of Number and CharSequence. 382 * 383 * @param name the name of the attribute. 384 * @param value the value of the attribute. 385 * @return a reference to this object. 386 * @since 4.5 387 */ 388 public XmlStringBuilder optAttributeCs(String name, CharSequence value) { 389 return optAttribute(name, value); 390 } 391 392 /** 393 * Add the given attribute if {@code value => 0}. 394 * 395 * @param name TODO javadoc me please 396 * @param value TODO javadoc me please 397 * @return a reference to this object 398 */ 399 public XmlStringBuilder optIntAttribute(String name, int value) { 400 if (value >= 0) { 401 attribute(name, Integer.toString(value)); 402 } 403 return this; 404 } 405 406 /** 407 * If the provided Integer argument is not null, then add a new XML attribute with the given name and the Integer as 408 * value. 409 * 410 * @param name the XML attribute name. 411 * @param value the optional integer to use as the attribute's value. 412 * @return a reference to this object. 413 * @since 4.4.1 414 */ 415 public XmlStringBuilder optIntAttribute(String name, Integer value) { 416 if (value != null) { 417 attribute(name, value.toString()); 418 } 419 return this; 420 } 421 422 /** 423 * Add the given attribute if value not null and {@code value => 0}. 424 * 425 * @param name TODO javadoc me please 426 * @param value TODO javadoc me please 427 * @return a reference to this object 428 */ 429 public XmlStringBuilder optLongAttribute(String name, Long value) { 430 if (value != null && value >= 0) { 431 attribute(name, Long.toString(value)); 432 } 433 return this; 434 } 435 436 public XmlStringBuilder optBooleanAttribute(String name, boolean bool) { 437 if (bool) { 438 sb.append(' ').append(name).append("='true'"); 439 } 440 return this; 441 } 442 443 public XmlStringBuilder optBooleanAttributeDefaultTrue(String name, boolean bool) { 444 if (!bool) { 445 sb.append(' ').append(name).append("='false'"); 446 } 447 return this; 448 } 449 450 private static final class XmlNsAttribute implements CharSequence { 451 private final String value; 452 private final String xmlFragment; 453 454 private XmlNsAttribute(String value) { 455 this.value = StringUtils.requireNotNullNorEmpty(value, "Value must not be null"); 456 this.xmlFragment = " xmlns='" + value + '\''; 457 } 458 459 @Override 460 public String toString() { 461 return xmlFragment; 462 } 463 464 @Override 465 public int length() { 466 return xmlFragment.length(); 467 } 468 469 @Override 470 public char charAt(int index) { 471 return xmlFragment.charAt(index); 472 } 473 474 @Override 475 public CharSequence subSequence(int start, int end) { 476 return xmlFragment.subSequence(start, end); 477 } 478 } 479 480 public XmlStringBuilder xmlnsAttribute(String value) { 481 if (value == null || (effectiveXmlEnvironment != null 482 && effectiveXmlEnvironment.effectiveNamespaceEquals(value))) { 483 return this; 484 } 485 XmlNsAttribute xmlNsAttribute = new XmlNsAttribute(value); 486 append(xmlNsAttribute); 487 return this; 488 } 489 490 public XmlStringBuilder xmllangAttribute(String value) { 491 // TODO: This should probably be attribute(), not optAttribute(). 492 optAttribute("xml:lang", value); 493 return this; 494 } 495 496 public XmlStringBuilder optXmlLangAttribute(String lang) { 497 if (!StringUtils.isNullOrEmpty(lang)) { 498 xmllangAttribute(lang); 499 } 500 return this; 501 } 502 503 public XmlStringBuilder text(CharSequence text) { 504 assert text != null; 505 CharSequence escapedText = StringUtils.escapeForXmlText(text); 506 sb.append(escapedText); 507 return this; 508 } 509 510 public XmlStringBuilder escape(String text) { 511 assert text != null; 512 sb.append(StringUtils.escapeForXml(text)); 513 return this; 514 } 515 516 public XmlStringBuilder escapeAttributeValue(String value) { 517 assert value != null; 518 sb.append(StringUtils.escapeForXmlAttributeApos(value)); 519 return this; 520 } 521 522 public XmlStringBuilder optEscape(CharSequence text) { 523 if (text == null) { 524 return this; 525 } 526 return escape(text); 527 } 528 529 public XmlStringBuilder escape(CharSequence text) { 530 return escape(text.toString()); 531 } 532 533 protected XmlStringBuilder prelude(XmlElement pe) { 534 return prelude(pe.getElementName(), pe.getNamespace()); 535 } 536 537 protected XmlStringBuilder prelude(String elementName, String namespace) { 538 halfOpenElement(elementName); 539 xmlnsAttribute(namespace); 540 return this; 541 } 542 543 public XmlStringBuilder optAppend(Element element) { 544 if (element != null) { 545 append(element.toXML(effectiveXmlEnvironment)); 546 } 547 return this; 548 } 549 550 public XmlStringBuilder optAppend(Collection<? extends Element> elements) { 551 if (elements != null) { 552 append(elements); 553 } 554 return this; 555 } 556 557 public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) { 558 if (sqc == null) { 559 return closeEmptyElement(); 560 } 561 rightAngleBracket(); 562 escape(sqc); 563 closeElement(parentElement); 564 return this; 565 } 566 567 public XmlStringBuilder append(XmlStringBuilder xsb) { 568 assert xsb != null; 569 sb.append(xsb.sb); 570 return this; 571 } 572 573 public XmlStringBuilder append(Element element) { 574 return append(element.toXML(effectiveXmlEnvironment)); 575 } 576 577 public XmlStringBuilder append(Collection<? extends Element> elements) { 578 for (Element element : elements) { 579 append(element); 580 } 581 return this; 582 } 583 584 public XmlStringBuilder emptyElement(Enum<?> element) { 585 // Use Enum.toString() instead Enum.name() here, since some enums override toString() in order to replace 586 // underscores ('_') with dash ('-') for example (name() is declared final in Enum). 587 return emptyElement(element.toString()); 588 } 589 590 public XmlStringBuilder emptyElement(String element) { 591 halfOpenElement(element); 592 return closeEmptyElement(); 593 } 594 595 public XmlStringBuilder condEmptyElement(boolean condition, String element) { 596 if (condition) { 597 emptyElement(element); 598 } 599 return this; 600 } 601 602 public XmlStringBuilder condAttribute(boolean condition, String name, String value) { 603 if (condition) { 604 attribute(name, value); 605 } 606 return this; 607 } 608 609 enum AppendApproach { 610 /** 611 * Simply add the given CharSequence to this builder. 612 */ 613 SINGLE, 614 615 /** 616 * If the given CharSequence is a {@link XmlStringBuilder} or {@link LazyStringBuilder}, then copy the 617 * references of the lazy strings parts into this builder. This approach flattens the string builders into one, 618 * yielding a different performance characteristic. 619 */ 620 FLAT, 621 } 622 623 private static AppendApproach APPEND_APPROACH = AppendApproach.SINGLE; 624 625 /** 626 * Set the builders approach on how to append new char sequences. 627 * 628 * @param appendApproach the append approach. 629 */ 630 public static void setAppendMethod(AppendApproach appendApproach) { 631 Objects.requireNonNull(appendApproach); 632 APPEND_APPROACH = appendApproach; 633 } 634 635 @Override 636 public XmlStringBuilder append(CharSequence csq) { 637 assert csq != null; 638 switch (APPEND_APPROACH) { 639 case SINGLE: 640 sb.append(csq); 641 break; 642 case FLAT: 643 if (csq instanceof XmlStringBuilder) { 644 sb.append(((XmlStringBuilder) csq).sb); 645 } else if (csq instanceof LazyStringBuilder) { 646 sb.append((LazyStringBuilder) csq); 647 } else { 648 sb.append(csq); 649 } 650 break; 651 } 652 return this; 653 } 654 655 @Override 656 public XmlStringBuilder append(CharSequence csq, int start, int end) { 657 assert csq != null; 658 sb.append(csq, start, end); 659 return this; 660 } 661 662 @Override 663 public XmlStringBuilder append(char c) { 664 sb.append(c); 665 return this; 666 } 667 668 @Override 669 public int length() { 670 return sb.length(); 671 } 672 673 @Override 674 public char charAt(int index) { 675 return sb.charAt(index); 676 } 677 678 @Override 679 public CharSequence subSequence(int start, int end) { 680 return sb.subSequence(start, end); 681 } 682 683 @Override 684 public String toString() { 685 return sb.toString(); 686 } 687 688 @Override 689 public boolean equals(Object other) { 690 if (!(other instanceof CharSequence)) { 691 return false; 692 } 693 CharSequence otherCharSequenceBuilder = (CharSequence) other; 694 return toString().equals(otherCharSequenceBuilder.toString()); 695 } 696 697 @Override 698 public int hashCode() { 699 return toString().hashCode(); 700 } 701 702 private static final class WrappedIoException extends RuntimeException { 703 704 private static final long serialVersionUID = 1L; 705 706 private final IOException wrappedIoException; 707 708 private WrappedIoException(IOException wrappedIoException) { 709 this.wrappedIoException = wrappedIoException; 710 } 711 } 712 713 /** 714 * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write 715 * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the 716 * XmlStringBuilder contents. 717 * 718 * @param writer TODO javadoc me please 719 * @param enclosingXmlEnvironment the enclosing XML environment. 720 * @throws IOException if an I/O error occurred. 721 */ 722 public void write(Writer writer, XmlEnvironment enclosingXmlEnvironment) throws IOException { 723 try { 724 appendXmlTo(csq -> { 725 try { 726 writer.append(csq); 727 } catch (IOException e) { 728 throw new WrappedIoException(e); 729 } 730 }, enclosingXmlEnvironment); 731 } catch (WrappedIoException e) { 732 throw e.wrappedIoException; 733 } 734 } 735 736 public List<CharSequence> toList(XmlEnvironment enclosingXmlEnvironment) { 737 List<CharSequence> res = new ArrayList<>(sb.getAsList().size()); 738 739 appendXmlTo(csq -> res.add(csq), enclosingXmlEnvironment); 740 741 return res; 742 } 743 744 @Override 745 public StringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { 746 // This is only the potential length, since the actual length depends on the given XmlEnvironment. 747 int potentialLength = length(); 748 StringBuilder res = new StringBuilder(potentialLength); 749 750 appendXmlTo(csq -> res.append(csq), enclosingXmlEnvironment); 751 752 return res; 753 } 754 755 private void appendXmlTo(Consumer<CharSequence> charSequenceSink, XmlEnvironment enclosingXmlEnvironment) { 756 for (CharSequence csq : sb.getAsList()) { 757 if (csq instanceof XmlStringBuilder) { 758 ((XmlStringBuilder) csq).appendXmlTo(charSequenceSink, enclosingXmlEnvironment); 759 } 760 else if (csq instanceof XmlNsAttribute) { 761 XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq; 762 if (!xmlNsAttribute.value.equals(enclosingXmlEnvironment.getEffectiveNamespace())) { 763 charSequenceSink.accept(xmlNsAttribute); 764 enclosingXmlEnvironment = new XmlEnvironment(xmlNsAttribute.value); 765 } 766 } 767 else { 768 charSequenceSink.accept(csq); 769 } 770 } 771 } 772}