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