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