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