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 * Add the given attribute if value not null and {@code value => 0}. 395 * 396 * @param name TODO javadoc me please 397 * @param value TODO javadoc me please 398 * @return a reference to this object 399 */ 400 public XmlStringBuilder optLongAttribute(String name, Long value) { 401 if (value != null && value >= 0) { 402 attribute(name, Long.toString(value)); 403 } 404 return this; 405 } 406 407 public XmlStringBuilder optBooleanAttribute(String name, boolean bool) { 408 if (bool) { 409 sb.append(' ').append(name).append("='true'"); 410 } 411 return this; 412 } 413 414 public XmlStringBuilder optBooleanAttributeDefaultTrue(String name, boolean bool) { 415 if (!bool) { 416 sb.append(' ').append(name).append("='false'"); 417 } 418 return this; 419 } 420 421 private static final class XmlNsAttribute implements CharSequence { 422 private final String value; 423 private final String xmlFragment; 424 425 private XmlNsAttribute(String value) { 426 this.value = StringUtils.requireNotNullNorEmpty(value, "Value must not be null"); 427 this.xmlFragment = " xmlns='" + value + '\''; 428 } 429 430 @Override 431 public String toString() { 432 return xmlFragment; 433 } 434 435 @Override 436 public int length() { 437 return xmlFragment.length(); 438 } 439 440 @Override 441 public char charAt(int index) { 442 return xmlFragment.charAt(index); 443 } 444 445 @Override 446 public CharSequence subSequence(int start, int end) { 447 return xmlFragment.subSequence(start, end); 448 } 449 } 450 451 public XmlStringBuilder xmlnsAttribute(String value) { 452 if (value == null || (effectiveXmlEnvironment != null 453 && effectiveXmlEnvironment.effectiveNamespaceEquals(value))) { 454 return this; 455 } 456 XmlNsAttribute xmlNsAttribute = new XmlNsAttribute(value); 457 append(xmlNsAttribute); 458 return this; 459 } 460 461 public XmlStringBuilder xmllangAttribute(String value) { 462 // TODO: This should probably be attribute(), not optAttribute(). 463 optAttribute("xml:lang", value); 464 return this; 465 } 466 467 public XmlStringBuilder optXmlLangAttribute(String lang) { 468 if (!StringUtils.isNullOrEmpty(lang)) { 469 xmllangAttribute(lang); 470 } 471 return this; 472 } 473 474 public XmlStringBuilder text(CharSequence text) { 475 assert text != null; 476 CharSequence escapedText = StringUtils.escapeForXmlText(text); 477 sb.append(escapedText); 478 return this; 479 } 480 481 public XmlStringBuilder escape(String text) { 482 assert text != null; 483 sb.append(StringUtils.escapeForXml(text)); 484 return this; 485 } 486 487 public XmlStringBuilder escapeAttributeValue(String value) { 488 assert value != null; 489 sb.append(StringUtils.escapeForXmlAttributeApos(value)); 490 return this; 491 } 492 493 public XmlStringBuilder optEscape(CharSequence text) { 494 if (text == null) { 495 return this; 496 } 497 return escape(text); 498 } 499 500 public XmlStringBuilder escape(CharSequence text) { 501 return escape(text.toString()); 502 } 503 504 protected XmlStringBuilder prelude(FullyQualifiedElement pe) { 505 return prelude(pe.getElementName(), pe.getNamespace()); 506 } 507 508 protected XmlStringBuilder prelude(String elementName, String namespace) { 509 halfOpenElement(elementName); 510 xmlnsAttribute(namespace); 511 return this; 512 } 513 514 public XmlStringBuilder optAppend(Element element) { 515 if (element != null) { 516 append(element.toXML(effectiveXmlEnvironment)); 517 } 518 return this; 519 } 520 521 public XmlStringBuilder optAppend(Collection<? extends Element> elements) { 522 if (elements != null) { 523 append(elements); 524 } 525 return this; 526 } 527 528 public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) { 529 if (sqc == null) { 530 return closeEmptyElement(); 531 } 532 rightAngleBracket(); 533 escape(sqc); 534 closeElement(parentElement); 535 return this; 536 } 537 538 public XmlStringBuilder append(XmlStringBuilder xsb) { 539 assert xsb != null; 540 sb.append(xsb.sb); 541 return this; 542 } 543 544 public XmlStringBuilder append(Element element) { 545 return append(element.toXML(effectiveXmlEnvironment)); 546 } 547 548 public XmlStringBuilder append(Collection<? extends Element> elements) { 549 for (Element element : elements) { 550 append(element); 551 } 552 return this; 553 } 554 555 public XmlStringBuilder emptyElement(Enum<?> element) { 556 // Use Enum.toString() instead Enum.name() here, since some enums override toString() in order to replace 557 // underscores ('_') with dash ('-') for example (name() is declared final in Enum). 558 return emptyElement(element.toString()); 559 } 560 561 public XmlStringBuilder emptyElement(String element) { 562 halfOpenElement(element); 563 return closeEmptyElement(); 564 } 565 566 public XmlStringBuilder condEmptyElement(boolean condition, String element) { 567 if (condition) { 568 emptyElement(element); 569 } 570 return this; 571 } 572 573 public XmlStringBuilder condAttribute(boolean condition, String name, String value) { 574 if (condition) { 575 attribute(name, value); 576 } 577 return this; 578 } 579 580 @Override 581 public XmlStringBuilder append(CharSequence csq) { 582 assert csq != null; 583 sb.append(csq); 584 return this; 585 } 586 587 @Override 588 public XmlStringBuilder append(CharSequence csq, int start, int end) { 589 assert csq != null; 590 sb.append(csq, start, end); 591 return this; 592 } 593 594 @Override 595 public XmlStringBuilder append(char c) { 596 sb.append(c); 597 return this; 598 } 599 600 @Override 601 public int length() { 602 return sb.length(); 603 } 604 605 @Override 606 public char charAt(int index) { 607 return sb.charAt(index); 608 } 609 610 @Override 611 public CharSequence subSequence(int start, int end) { 612 return sb.subSequence(start, end); 613 } 614 615 @Override 616 public String toString() { 617 return sb.toString(); 618 } 619 620 @Override 621 public boolean equals(Object other) { 622 if (!(other instanceof CharSequence)) { 623 return false; 624 } 625 CharSequence otherCharSequenceBuilder = (CharSequence) other; 626 return toString().equals(otherCharSequenceBuilder.toString()); 627 } 628 629 @Override 630 public int hashCode() { 631 return toString().hashCode(); 632 } 633 634 private static final class WrappedIoException extends RuntimeException { 635 636 private static final long serialVersionUID = 1L; 637 638 private final IOException wrappedIoException; 639 640 private WrappedIoException(IOException wrappedIoException) { 641 this.wrappedIoException = wrappedIoException; 642 } 643 } 644 645 /** 646 * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write 647 * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the 648 * XmlStringBuilder contents. 649 * 650 * @param writer TODO javadoc me please 651 * @param enclosingXmlEnvironment the enclosing XML environment. 652 * @throws IOException if an I/O error occurred. 653 */ 654 public void write(Writer writer, XmlEnvironment enclosingXmlEnvironment) throws IOException { 655 try { 656 appendXmlTo(csq -> { 657 try { 658 writer.append(csq); 659 } catch (IOException e) { 660 throw new WrappedIoException(e); 661 } 662 }, enclosingXmlEnvironment); 663 } catch (WrappedIoException e) { 664 throw e.wrappedIoException; 665 } 666 } 667 668 public List<CharSequence> toList(XmlEnvironment enclosingXmlEnvironment) { 669 List<CharSequence> res = new ArrayList<>(sb.getAsList().size()); 670 671 appendXmlTo(csq -> res.add(csq), enclosingXmlEnvironment); 672 673 return res; 674 } 675 676 @Override 677 public StringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) { 678 // This is only the potential length, since the actual length depends on the given XmlEnvironment. 679 int potentialLength = length(); 680 StringBuilder res = new StringBuilder(potentialLength); 681 682 appendXmlTo(csq -> res.append(csq), enclosingXmlEnvironment); 683 684 return res; 685 } 686 687 private void appendXmlTo(Consumer<CharSequence> charSequenceSink, XmlEnvironment enclosingXmlEnvironment) { 688 for (CharSequence csq : sb.getAsList()) { 689 if (csq instanceof XmlStringBuilder) { 690 ((XmlStringBuilder) csq).appendXmlTo(charSequenceSink, enclosingXmlEnvironment); 691 } 692 else if (csq instanceof XmlNsAttribute) { 693 XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq; 694 if (!xmlNsAttribute.value.equals(enclosingXmlEnvironment.getEffectiveNamespace())) { 695 charSequenceSink.accept(xmlNsAttribute); 696 enclosingXmlEnvironment = new XmlEnvironment(xmlNsAttribute.value); 697 } 698 } 699 else { 700 charSequenceSink.accept(csq); 701 } 702 } 703 } 704}