001/** 002 * 003 * Copyright 2014-2018 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.Collection; 022import java.util.Date; 023 024import org.jivesoftware.smack.packet.Element; 025import org.jivesoftware.smack.packet.ExtensionElement; 026import org.jivesoftware.smack.packet.NamedElement; 027 028import org.jxmpp.util.XmppDateTime; 029 030public class XmlStringBuilder implements Appendable, CharSequence, Element { 031 public static final String RIGHT_ANGLE_BRACKET = Character.toString('>'); 032 033 private final LazyStringBuilder sb; 034 035 private final String enclosingNamespace; 036 037 public XmlStringBuilder() { 038 this(""); 039 } 040 041 public XmlStringBuilder(String enclosingNamespace) { 042 sb = new LazyStringBuilder(); 043 this.enclosingNamespace = enclosingNamespace != null ? enclosingNamespace : ""; 044 } 045 046 public XmlStringBuilder(ExtensionElement pe) { 047 this(); 048 prelude(pe); 049 } 050 051 public XmlStringBuilder(NamedElement e) { 052 this(); 053 halfOpenElement(e.getElementName()); 054 } 055 056 public XmlStringBuilder(ExtensionElement ee, String enclosingNamespace) { 057 this(enclosingNamespace); 058 String namespace = ee.getNamespace(); 059 prelude(ee); 060 } 061 062 public XmlStringBuilder escapedElement(String name, String escapedContent) { 063 assert escapedContent != null; 064 openElement(name); 065 append(escapedContent); 066 closeElement(name); 067 return this; 068 } 069 070 /** 071 * Add a new element to this builder. 072 * 073 * @param name 074 * @param content 075 * @return the XmlStringBuilder 076 */ 077 public XmlStringBuilder element(String name, String content) { 078 if (content.isEmpty()) { 079 return emptyElement(name); 080 } 081 openElement(name); 082 escape(content); 083 closeElement(name); 084 return this; 085 } 086 087 /** 088 * Add a new element to this builder, with the {@link java.util.Date} instance as its content, 089 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}. 090 * 091 * @param name element name 092 * @param content content of element 093 * @return this XmlStringBuilder 094 */ 095 public XmlStringBuilder element(String name, Date content) { 096 assert content != null; 097 return element(name, XmppDateTime.formatXEP0082Date(content)); 098 } 099 100 /** 101 * Add a new element to this builder. 102 * 103 * @param name 104 * @param content 105 * @return the XmlStringBuilder 106 */ 107 public XmlStringBuilder element(String name, CharSequence content) { 108 return element(name, content.toString()); 109 } 110 111 public XmlStringBuilder element(String name, Enum<?> content) { 112 assert content != null; 113 element(name, content.name()); 114 return this; 115 } 116 117 public XmlStringBuilder element(Element element) { 118 assert element != null; 119 return append(element.toXML(null)); 120 } 121 122 public XmlStringBuilder optElement(String name, String content) { 123 if (content != null) { 124 element(name, content); 125 } 126 return this; 127 } 128 129 /** 130 * Add a new element to this builder, with the {@link java.util.Date} instance as its content, 131 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)} 132 * if {@link java.util.Date} instance is not <code>null</code>. 133 * 134 * @param name element name 135 * @param content content of element 136 * @return this XmlStringBuilder 137 */ 138 public XmlStringBuilder optElement(String name, Date content) { 139 if (content != null) { 140 element(name, content); 141 } 142 return this; 143 } 144 145 public XmlStringBuilder optElement(String name, CharSequence content) { 146 if (content != null) { 147 element(name, content.toString()); 148 } 149 return this; 150 } 151 152 public XmlStringBuilder optElement(Element element) { 153 if (element != null) { 154 append(element.toXML(null)); 155 } 156 return this; 157 } 158 159 public XmlStringBuilder optElement(String name, Enum<?> content) { 160 if (content != null) { 161 element(name, content); 162 } 163 return this; 164 } 165 166 public XmlStringBuilder optElement(String name, Object object) { 167 if (object != null) { 168 element(name, object.toString()); 169 } 170 return this; 171 } 172 173 public XmlStringBuilder optIntElement(String name, int value) { 174 if (value >= 0) { 175 element(name, String.valueOf(value)); 176 } 177 return this; 178 } 179 180 public XmlStringBuilder halfOpenElement(String name) { 181 assert (StringUtils.isNotEmpty(name)); 182 sb.append('<').append(name); 183 return this; 184 } 185 186 public XmlStringBuilder halfOpenElement(NamedElement namedElement) { 187 return halfOpenElement(namedElement.getElementName()); 188 } 189 190 public XmlStringBuilder openElement(String name) { 191 halfOpenElement(name).rightAngleBracket(); 192 return this; 193 } 194 195 public XmlStringBuilder closeElement(String name) { 196 sb.append("</").append(name); 197 rightAngleBracket(); 198 return this; 199 } 200 201 public XmlStringBuilder closeElement(NamedElement e) { 202 closeElement(e.getElementName()); 203 return this; 204 } 205 206 public XmlStringBuilder closeEmptyElement() { 207 sb.append("/>"); 208 return this; 209 } 210 211 /** 212 * Add a right angle bracket '>'. 213 * 214 * @return a reference to this object. 215 */ 216 public XmlStringBuilder rightAngleBracket() { 217 sb.append(RIGHT_ANGLE_BRACKET); 218 return this; 219 } 220 221 /** 222 * Add a right angle bracket '>'. 223 * 224 * @return a reference to this object 225 * @deprecated use {@link #rightAngleBracket()} instead 226 */ 227 @Deprecated 228 public XmlStringBuilder rightAngelBracket() { 229 return rightAngleBracket(); 230 } 231 232 /** 233 * Does nothing if value is null. 234 * 235 * @param name 236 * @param value 237 * @return the XmlStringBuilder 238 */ 239 public XmlStringBuilder attribute(String name, String value) { 240 assert value != null; 241 sb.append(' ').append(name).append("='"); 242 escapeAttributeValue(value); 243 sb.append('\''); 244 return this; 245 } 246 247 public XmlStringBuilder attribute(String name, boolean bool) { 248 return attribute(name, Boolean.toString(bool)); 249 } 250 251 /** 252 * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value, 253 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}. 254 * 255 * @param name name of attribute 256 * @param value value of attribute 257 * @return this XmlStringBuilder 258 */ 259 public XmlStringBuilder attribute(String name, Date value) { 260 assert value != null; 261 return attribute(name, XmppDateTime.formatXEP0082Date(value)); 262 } 263 264 public XmlStringBuilder attribute(String name, CharSequence value) { 265 return attribute(name, value.toString()); 266 } 267 268 public XmlStringBuilder attribute(String name, Enum<?> value) { 269 assert value != null; 270 attribute(name, value.name()); 271 return this; 272 } 273 274 public XmlStringBuilder attribute(String name, int value) { 275 assert name != null; 276 return attribute(name, String.valueOf(value)); 277 } 278 279 public XmlStringBuilder optAttribute(String name, String value) { 280 if (value != null) { 281 attribute(name, value); 282 } 283 return this; 284 } 285 286 /** 287 * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value, 288 * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)} 289 * if {@link java.util.Date} instance is not <code>null</code>. 290 * 291 * @param name attribute name 292 * @param value value of this attribute 293 * @return this XmlStringBuilder 294 */ 295 public XmlStringBuilder optAttribute(String name, Date value) { 296 if (value != null) { 297 attribute(name, value); 298 } 299 return this; 300 } 301 302 public XmlStringBuilder optAttribute(String name, CharSequence value) { 303 if (value != null) { 304 attribute(name, value.toString()); 305 } 306 return this; 307 } 308 309 public XmlStringBuilder optAttribute(String name, Enum<?> value) { 310 if (value != null) { 311 attribute(name, value.toString()); 312 } 313 return this; 314 } 315 316 /** 317 * Add the given attribute if {@code value => 0}. 318 * 319 * @param name 320 * @param value 321 * @return a reference to this object 322 */ 323 public XmlStringBuilder optIntAttribute(String name, int value) { 324 if (value >= 0) { 325 attribute(name, Integer.toString(value)); 326 } 327 return this; 328 } 329 330 /** 331 * Add the given attribute if value not null and {@code value => 0}. 332 * 333 * @param name 334 * @param value 335 * @return a reference to this object 336 */ 337 public XmlStringBuilder optLongAttribute(String name, Long value) { 338 if (value != null && value >= 0) { 339 attribute(name, Long.toString(value)); 340 } 341 return this; 342 } 343 344 public XmlStringBuilder optBooleanAttribute(String name, boolean bool) { 345 if (bool) { 346 sb.append(' ').append(name).append("='true'"); 347 } 348 return this; 349 } 350 351 public XmlStringBuilder optBooleanAttributeDefaultTrue(String name, boolean bool) { 352 if (!bool) { 353 sb.append(' ').append(name).append("='false'"); 354 } 355 return this; 356 } 357 358 private static final class XmlNsAttribute implements CharSequence { 359 private final String value; 360 private final String xmlFragment; 361 362 private XmlNsAttribute(String value) { 363 this.value = StringUtils.requireNotNullOrEmpty(value, "Value must not be null"); 364 this.xmlFragment = " xmlns='" + value + '\''; 365 } 366 367 @Override 368 public String toString() { 369 return xmlFragment; 370 } 371 372 @Override 373 public int length() { 374 return xmlFragment.length(); 375 } 376 377 @Override 378 public char charAt(int index) { 379 return xmlFragment.charAt(index); 380 } 381 382 @Override 383 public CharSequence subSequence(int start, int end) { 384 return xmlFragment.subSequence(start, end); 385 } 386 } 387 388 public XmlStringBuilder xmlnsAttribute(String value) { 389 if (value != null && !enclosingNamespace.equals(value)) { 390 XmlNsAttribute xmlNsAttribute = new XmlNsAttribute(value); 391 append(xmlNsAttribute); 392 } 393 return this; 394 } 395 396 public XmlStringBuilder xmllangAttribute(String value) { 397 optAttribute("xml:lang", value); 398 return this; 399 } 400 401 public XmlStringBuilder optXmlLangAttribute(String lang) { 402 if (!StringUtils.isNullOrEmpty(lang)) { 403 xmllangAttribute(lang); 404 } 405 return this; 406 } 407 408 public XmlStringBuilder escape(String text) { 409 assert text != null; 410 sb.append(StringUtils.escapeForXml(text)); 411 return this; 412 } 413 414 public XmlStringBuilder escapeAttributeValue(String value) { 415 assert value != null; 416 sb.append(StringUtils.escapeForXmlAttributeApos(value)); 417 return this; 418 } 419 420 public XmlStringBuilder optEscape(CharSequence text) { 421 if (text == null) { 422 return this; 423 } 424 return escape(text); 425 } 426 427 public XmlStringBuilder escape(CharSequence text) { 428 return escape(text.toString()); 429 } 430 431 public XmlStringBuilder prelude(ExtensionElement pe) { 432 return prelude(pe.getElementName(), pe.getNamespace()); 433 } 434 435 public XmlStringBuilder prelude(String elementName, String namespace) { 436 halfOpenElement(elementName); 437 xmlnsAttribute(namespace); 438 return this; 439 } 440 441 public XmlStringBuilder optAppend(CharSequence csq) { 442 if (csq != null) { 443 append(csq); 444 } 445 return this; 446 } 447 448 public XmlStringBuilder optAppend(Element element) { 449 if (element != null) { 450 append(element.toXML(enclosingNamespace)); 451 } 452 return this; 453 } 454 455 public XmlStringBuilder append(XmlStringBuilder xsb) { 456 assert xsb != null; 457 sb.append(xsb.sb); 458 return this; 459 } 460 461 public XmlStringBuilder append(Collection<? extends Element> elements) { 462 return append(elements, null); 463 } 464 465 public XmlStringBuilder append(Collection<? extends Element> elements, String enclosingNamespace) { 466 for (Element element : elements) { 467 append(element.toXML(enclosingNamespace)); 468 } 469 return this; 470 } 471 472 public XmlStringBuilder emptyElement(Enum<?> element) { 473 return emptyElement(element.name()); 474 } 475 476 public XmlStringBuilder emptyElement(String element) { 477 halfOpenElement(element); 478 return closeEmptyElement(); 479 } 480 481 public XmlStringBuilder condEmptyElement(boolean condition, String element) { 482 if (condition) { 483 emptyElement(element); 484 } 485 return this; 486 } 487 488 public XmlStringBuilder condAttribute(boolean condition, String name, String value) { 489 if (condition) { 490 attribute(name, value); 491 } 492 return this; 493 } 494 495 @Override 496 public XmlStringBuilder append(CharSequence csq) { 497 assert csq != null; 498 sb.append(csq); 499 return this; 500 } 501 502 @Override 503 public XmlStringBuilder append(CharSequence csq, int start, int end) { 504 assert csq != null; 505 sb.append(csq, start, end); 506 return this; 507 } 508 509 @Override 510 public XmlStringBuilder append(char c) { 511 sb.append(c); 512 return this; 513 } 514 515 @Override 516 public int length() { 517 return sb.length(); 518 } 519 520 @Override 521 public char charAt(int index) { 522 return sb.charAt(index); 523 } 524 525 @Override 526 public CharSequence subSequence(int start, int end) { 527 return sb.subSequence(start, end); 528 } 529 530 @Override 531 public String toString() { 532 return sb.toString(); 533 } 534 535 @Override 536 public boolean equals(Object other) { 537 if (!(other instanceof CharSequence)) { 538 return false; 539 } 540 CharSequence otherCharSequenceBuilder = (CharSequence) other; 541 return toString().equals(otherCharSequenceBuilder.toString()); 542 } 543 544 @Override 545 public int hashCode() { 546 return toString().hashCode(); 547 } 548 549 /** 550 * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write 551 * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the 552 * XmlStringBuilder contents. 553 * 554 * @param writer 555 * @throws IOException 556 */ 557 public void write(Writer writer, String enclosingNamespace) throws IOException { 558 for (CharSequence csq : sb.getAsList()) { 559 if (csq instanceof XmlStringBuilder) { 560 ((XmlStringBuilder) csq).write(writer, enclosingNamespace); 561 } 562 else if (csq instanceof XmlNsAttribute) { 563 XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq; 564 if (!xmlNsAttribute.value.equals(enclosingNamespace)) { 565 writer.write(xmlNsAttribute.toString()); 566 enclosingNamespace = xmlNsAttribute.value; 567 } 568 } 569 else { 570 writer.write(csq.toString()); 571 } 572 } 573 } 574 575 @Override 576 public CharSequence toXML(String enclosingNamespace) { 577 StringBuilder res = new StringBuilder(); 578 appendXmlTo(res, enclosingNamespace); 579 return res; 580 } 581 582 private void appendXmlTo(StringBuilder res, String enclosingNamespace) { 583 for (CharSequence csq : sb.getAsList()) { 584 if (csq instanceof XmlStringBuilder) { 585 ((XmlStringBuilder) csq).appendXmlTo(res, enclosingNamespace); 586 } 587 else if (csq instanceof XmlNsAttribute) { 588 XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq; 589 if (!xmlNsAttribute.value.equals(enclosingNamespace)) { 590 sb.append(xmlNsAttribute); 591 enclosingNamespace = xmlNsAttribute.value; 592 } 593 } 594 else { 595 res.append(csq); 596 } 597 } 598 } 599}