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