001/** 002 * 003 * Copyright 2014-2020 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.ExtensionElement; 028import org.jivesoftware.smack.packet.FullyQualifiedElement; 029import org.jivesoftware.smack.packet.NamedElement; 030import org.jivesoftware.smack.packet.XmlEnvironment; 031 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(ExtensionElement pe) { 047 this(pe, null); 048 } 049 050 public XmlStringBuilder(NamedElement e) { 051 this(); 052 halfOpenElement(e.getElementName()); 053 } 054 055 public XmlStringBuilder(FullyQualifiedElement element, XmlEnvironment enclosingXmlEnvironment) { 056 sb = new LazyStringBuilder(); 057 halfOpenElement(element); 058 059 String xmlNs = element.getNamespace(); 060 String xmlLang = element.getLanguage(); 061 if (enclosingXmlEnvironment == null) { 062 xmlnsAttribute(xmlNs); 063 xmllangAttribute(xmlLang); 064 } else { 065 if (!enclosingXmlEnvironment.effectiveNamespaceEquals(xmlNs)) { 066 xmlnsAttribute(xmlNs); 067 } 068 if (!enclosingXmlEnvironment.effectiveLanguageEquals(xmlLang)) { 069 xmllangAttribute(xmlLang); 070 } 071 } 072 073 effectiveXmlEnvironment = XmlEnvironment.builder() 074 .withNamespace(xmlNs) 075 .withLanguage(xmlLang) 076 .withNext(enclosingXmlEnvironment) 077 .build(); 078 } 079 080 public XmlEnvironment getXmlEnvironment() { 081 return effectiveXmlEnvironment; 082 } 083 084 public XmlStringBuilder escapedElement(String name, String escapedContent) { 085 assert escapedContent != null; 086 openElement(name); 087 append(escapedContent); 088 closeElement(name); 089 return this; 090 } 091 092 /** 093 * Add a new element to this builder. 094 * 095 * @param name TODO javadoc me please 096 * @param content TODO javadoc me please 097 * @return the XmlStringBuilder 098 */ 099 public XmlStringBuilder element(String name, String content) { 100 if (content.isEmpty()) { 101 return emptyElement(name); 102 } 103 openElement(name); 104 escape(content); 105 closeElement(name); 106 return this; 107 } 108 109 /** 110 * Add a new element to this builder, with the {@link java.util.Date} instance as its content, 111 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}. 112 * 113 * @param name element name 114 * @param content content of element 115 * @return this XmlStringBuilder 116 */ 117 public XmlStringBuilder element(String name, Date content) { 118 assert content != null; 119 return element(name, XmppDateTime.formatXEP0082Date(content)); 120 } 121 122 /** 123 * Add a new element to this builder. 124 * 125 * @param name TODO javadoc me please 126 * @param content TODO javadoc me please 127 * @return the XmlStringBuilder 128 */ 129 public XmlStringBuilder element(String name, CharSequence content) { 130 return element(name, content.toString()); 131 } 132 133 public XmlStringBuilder element(String name, Enum<?> content) { 134 assert content != null; 135 element(name, content.toString()); 136 return this; 137 } 138 139 /** 140 * Deprecated. 141 * 142 * @param element deprecated. 143 * @return deprecated. 144 * @deprecated use {@link #append(Element)} instead. 145 */ 146 @Deprecated 147 // TODO: Remove in Smack 4.5. 148 public XmlStringBuilder element(Element element) { 149 assert element != null; 150 return append(element.toXML()); 151 } 152 153 public XmlStringBuilder optElement(String name, String content) { 154 if (content != null) { 155 element(name, content); 156 } 157 return this; 158 } 159 160 /** 161 * Add a new element to this builder, with the {@link java.util.Date} instance as its content, 162 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)} 163 * if {@link java.util.Date} instance is not <code>null</code>. 164 * 165 * @param name element name 166 * @param content content of element 167 * @return this XmlStringBuilder 168 */ 169 public XmlStringBuilder optElement(String name, Date content) { 170 if (content != null) { 171 element(name, content); 172 } 173 return this; 174 } 175 176 public XmlStringBuilder optElement(String name, CharSequence content) { 177 if (content != null) { 178 element(name, content.toString()); 179 } 180 return this; 181 } 182 183 public XmlStringBuilder optElement(Element element) { 184 if (element != null) { 185 append(element); 186 } 187 return this; 188 } 189 190 public XmlStringBuilder optElement(String name, Enum<?> content) { 191 if (content != null) { 192 element(name, content); 193 } 194 return this; 195 } 196 197 public XmlStringBuilder optElement(String name, Object object) { 198 if (object != null) { 199 element(name, object.toString()); 200 } 201 return this; 202 } 203 204 public XmlStringBuilder optIntElement(String name, int value) { 205 if (value >= 0) { 206 element(name, String.valueOf(value)); 207 } 208 return this; 209 } 210 211 public XmlStringBuilder halfOpenElement(String name) { 212 assert StringUtils.isNotEmpty(name); 213 sb.append('<').append(name); 214 return this; 215 } 216 217 public XmlStringBuilder halfOpenElement(NamedElement namedElement) { 218 return halfOpenElement(namedElement.getElementName()); 219 } 220 221 public XmlStringBuilder openElement(String name) { 222 halfOpenElement(name).rightAngleBracket(); 223 return this; 224 } 225 226 public XmlStringBuilder closeElement(String name) { 227 sb.append("</").append(name); 228 rightAngleBracket(); 229 return this; 230 } 231 232 public XmlStringBuilder closeElement(NamedElement e) { 233 closeElement(e.getElementName()); 234 return this; 235 } 236 237 public XmlStringBuilder closeEmptyElement() { 238 sb.append("/>"); 239 return this; 240 } 241 242 /** 243 * Add a right angle bracket '>'. 244 * 245 * @return a reference to this object. 246 */ 247 public XmlStringBuilder rightAngleBracket() { 248 sb.append(RIGHT_ANGLE_BRACKET); 249 return this; 250 } 251 252 /** 253 * Does nothing if value is null. 254 * 255 * @param name TODO javadoc me please 256 * @param value TODO javadoc me please 257 * @return the XmlStringBuilder 258 */ 259 public XmlStringBuilder attribute(String name, String value) { 260 assert value != null; 261 sb.append(' ').append(name).append("='"); 262 escapeAttributeValue(value); 263 sb.append('\''); 264 return this; 265 } 266 267 public XmlStringBuilder attribute(String name, boolean bool) { 268 return attribute(name, Boolean.toString(bool)); 269 } 270 271 /** 272 * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value, 273 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}. 274 * 275 * @param name name of attribute 276 * @param value value of attribute 277 * @return this XmlStringBuilder 278 */ 279 public XmlStringBuilder attribute(String name, Date value) { 280 assert value != null; 281 return attribute(name, XmppDateTime.formatXEP0082Date(value)); 282 } 283 284 public XmlStringBuilder attribute(String name, CharSequence value) { 285 return attribute(name, value.toString()); 286 } 287 288 public XmlStringBuilder attribute(String name, Enum<?> value) { 289 assert value != null; 290 // TODO: Should use toString() instead of name(). 291 attribute(name, value.name()); 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(FullyQualifiedElement 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 @Override 597 public XmlStringBuilder append(CharSequence csq) { 598 assert csq != null; 599 sb.append(csq); 600 return this; 601 } 602 603 @Override 604 public XmlStringBuilder append(CharSequence csq, int start, int end) { 605 assert csq != null; 606 sb.append(csq, start, end); 607 return this; 608 } 609 610 @Override 611 public XmlStringBuilder append(char c) { 612 sb.append(c); 613 return this; 614 } 615 616 @Override 617 public int length() { 618 return sb.length(); 619 } 620 621 @Override 622 public char charAt(int index) { 623 return sb.charAt(index); 624 } 625 626 @Override 627 public CharSequence subSequence(int start, int end) { 628 return sb.subSequence(start, end); 629 } 630 631 @Override 632 public String toString() { 633 return sb.toString(); 634 } 635 636 @Override 637 public boolean equals(Object other) { 638 if (!(other instanceof CharSequence)) { 639 return false; 640 } 641 CharSequence otherCharSequenceBuilder = (CharSequence) other; 642 return toString().equals(otherCharSequenceBuilder.toString()); 643 } 644 645 @Override 646 public int hashCode() { 647 return toString().hashCode(); 648 } 649 650 private static final class WrappedIoException extends RuntimeException { 651 652 private static final long serialVersionUID = 1L; 653 654 private final IOException wrappedIoException; 655 656 private WrappedIoException(IOException wrappedIoException) { 657 this.wrappedIoException = wrappedIoException; 658 } 659 } 660 661 /** 662 * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write 663 * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the 664 * XmlStringBuilder contents. 665 * 666 * @param writer TODO javadoc me please 667 * @param enclosingXmlEnvironment the enclosing XML environment. 668 * @throws IOException if an I/O error occurred. 669 */ 670 public void write(Writer writer, XmlEnvironment enclosingXmlEnvironment) throws IOException { 671 try { 672 appendXmlTo(csq -> { 673 try { 674 writer.append(csq); 675 } catch (IOException e) { 676 throw new WrappedIoException(e); 677 } 678 }, enclosingXmlEnvironment); 679 } catch (WrappedIoException e) { 680 throw e.wrappedIoException; 681 } 682 } 683 684 public List<CharSequence> toList(XmlEnvironment enclosingXmlEnvironment) { 685 List<CharSequence> res = new ArrayList<>(sb.getAsList().size()); 686 687 appendXmlTo(csq -> res.add(csq), enclosingXmlEnvironment); 688 689 return res; 690 } 691 692 @Override 693 public StringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { 694 // This is only the potential length, since the actual length depends on the given XmlEnvironment. 695 int potentialLength = length(); 696 StringBuilder res = new StringBuilder(potentialLength); 697 698 appendXmlTo(csq -> res.append(csq), enclosingXmlEnvironment); 699 700 return res; 701 } 702 703 private void appendXmlTo(Consumer<CharSequence> charSequenceSink, XmlEnvironment enclosingXmlEnvironment) { 704 for (CharSequence csq : sb.getAsList()) { 705 if (csq instanceof XmlStringBuilder) { 706 ((XmlStringBuilder) csq).appendXmlTo(charSequenceSink, enclosingXmlEnvironment); 707 } 708 else if (csq instanceof XmlNsAttribute) { 709 XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq; 710 if (!xmlNsAttribute.value.equals(enclosingXmlEnvironment.getEffectiveNamespace())) { 711 charSequenceSink.accept(xmlNsAttribute); 712 enclosingXmlEnvironment = new XmlEnvironment(xmlNsAttribute.value); 713 } 714 } 715 else { 716 charSequenceSink.accept(csq); 717 } 718 } 719 } 720}