XmlStringBuilder.java
- /**
- *
- * Copyright 2014-2024 Florian Schmaus
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.jivesoftware.smack.util;
- import java.io.IOException;
- import java.io.Writer;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Date;
- import java.util.List;
- import org.jivesoftware.smack.packet.Element;
- import org.jivesoftware.smack.packet.NamedElement;
- import org.jivesoftware.smack.packet.XmlElement;
- import org.jivesoftware.smack.packet.XmlEnvironment;
- import org.jxmpp.jid.Jid;
- import org.jxmpp.util.XmppDateTime;
- public class XmlStringBuilder implements Appendable, CharSequence, Element {
- public static final String RIGHT_ANGLE_BRACKET = Character.toString('>');
- private final LazyStringBuilder sb;
- private final XmlEnvironment effectiveXmlEnvironment;
- public XmlStringBuilder() {
- sb = new LazyStringBuilder();
- effectiveXmlEnvironment = null;
- }
- public XmlStringBuilder(XmlElement pe) {
- this(pe, null);
- }
- public XmlStringBuilder(NamedElement e) {
- this();
- halfOpenElement(e.getElementName());
- }
- public XmlStringBuilder(XmlElement element, XmlEnvironment enclosingXmlEnvironment) {
- this(element.getElementName(), element.getNamespace(), element.getLanguage(), enclosingXmlEnvironment);
- }
- public XmlStringBuilder(String elementName, String xmlNs, String xmlLang, XmlEnvironment enclosingXmlEnvironment) {
- sb = new LazyStringBuilder();
- halfOpenElement(elementName);
- if (enclosingXmlEnvironment == null) {
- xmlnsAttribute(xmlNs);
- xmllangAttribute(xmlLang);
- } else {
- if (!enclosingXmlEnvironment.effectiveNamespaceEquals(xmlNs)) {
- xmlnsAttribute(xmlNs);
- }
- if (!enclosingXmlEnvironment.effectiveLanguageEquals(xmlLang)) {
- xmllangAttribute(xmlLang);
- }
- }
- effectiveXmlEnvironment = XmlEnvironment.builder()
- .withNamespace(xmlNs)
- .withLanguage(xmlLang)
- .withNext(enclosingXmlEnvironment)
- .build();
- }
- public XmlEnvironment getXmlEnvironment() {
- return effectiveXmlEnvironment;
- }
- public XmlStringBuilder escapedElement(String name, String escapedContent) {
- assert escapedContent != null;
- openElement(name);
- append(escapedContent);
- closeElement(name);
- return this;
- }
- /**
- * Add a new element to this builder.
- *
- * @param name TODO javadoc me please
- * @param content TODO javadoc me please
- * @return the XmlStringBuilder
- */
- public XmlStringBuilder element(String name, String content) {
- if (content.isEmpty()) {
- return emptyElement(name);
- }
- openElement(name);
- escape(content);
- closeElement(name);
- return this;
- }
- /**
- * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
- * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
- *
- * @param name element name
- * @param content content of element
- * @return this XmlStringBuilder
- */
- public XmlStringBuilder element(String name, Date content) {
- assert content != null;
- return element(name, XmppDateTime.formatXEP0082Date(content));
- }
- /**
- * Add a new element to this builder.
- *
- * @param name TODO javadoc me please
- * @param content TODO javadoc me please
- * @return the XmlStringBuilder
- */
- public XmlStringBuilder element(String name, CharSequence content) {
- return element(name, content.toString());
- }
- public XmlStringBuilder element(String name, Enum<?> content) {
- assert content != null;
- element(name, content.toString());
- return this;
- }
- public XmlStringBuilder optElement(String name, String content) {
- if (content != null) {
- element(name, content);
- }
- return this;
- }
- /**
- * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
- * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
- * if {@link java.util.Date} instance is not <code>null</code>.
- *
- * @param name element name
- * @param content content of element
- * @return this XmlStringBuilder
- */
- public XmlStringBuilder optElement(String name, Date content) {
- if (content != null) {
- element(name, content);
- }
- return this;
- }
- public XmlStringBuilder optElement(String name, CharSequence content) {
- if (content != null) {
- element(name, content.toString());
- }
- return this;
- }
- public XmlStringBuilder optElement(Element element) {
- if (element != null) {
- append(element);
- }
- return this;
- }
- public XmlStringBuilder optElement(String name, Enum<?> content) {
- if (content != null) {
- element(name, content);
- }
- return this;
- }
- public XmlStringBuilder optElement(String name, Object object) {
- if (object != null) {
- element(name, object.toString());
- }
- return this;
- }
- public XmlStringBuilder optIntElement(String name, int value) {
- if (value >= 0) {
- element(name, String.valueOf(value));
- }
- return this;
- }
- public XmlStringBuilder halfOpenElement(String name) {
- assert StringUtils.isNotEmpty(name);
- sb.append('<').append(name);
- return this;
- }
- public XmlStringBuilder halfOpenElement(NamedElement namedElement) {
- return halfOpenElement(namedElement.getElementName());
- }
- public XmlStringBuilder openElement(String name) {
- halfOpenElement(name).rightAngleBracket();
- return this;
- }
- public XmlStringBuilder closeElement(String name) {
- sb.append("</").append(name);
- rightAngleBracket();
- return this;
- }
- public XmlStringBuilder closeElement(NamedElement e) {
- closeElement(e.getElementName());
- return this;
- }
- public XmlStringBuilder closeEmptyElement() {
- sb.append("/>");
- return this;
- }
- /**
- * Add a right angle bracket '>'.
- *
- * @return a reference to this object.
- */
- public XmlStringBuilder rightAngleBracket() {
- sb.append(RIGHT_ANGLE_BRACKET);
- return this;
- }
- /**
- * Does nothing if value is null.
- *
- * @param name TODO javadoc me please
- * @param value TODO javadoc me please
- * @return the XmlStringBuilder
- */
- public XmlStringBuilder attribute(String name, String value) {
- assert value != null;
- sb.append(' ').append(name).append("='");
- escapeAttributeValue(value);
- sb.append('\'');
- return this;
- }
- public XmlStringBuilder attribute(String name, boolean bool) {
- return attribute(name, Boolean.toString(bool));
- }
- /**
- * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
- * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
- *
- * @param name name of attribute
- * @param value value of attribute
- * @return this XmlStringBuilder
- */
- public XmlStringBuilder attribute(String name, Date value) {
- assert value != null;
- return attribute(name, XmppDateTime.formatXEP0082Date(value));
- }
- public XmlStringBuilder attribute(String name, CharSequence value) {
- return attribute(name, value.toString());
- }
- public XmlStringBuilder attribute(String name, Enum<?> value) {
- assert value != null;
- attribute(name, value.toString());
- return this;
- }
- public <E extends Enum<?>> XmlStringBuilder attribute(String name, E value, E implicitDefault) {
- if (value == null || value == implicitDefault) {
- return this;
- }
- attribute(name, value.toString());
- return this;
- }
- public XmlStringBuilder attribute(String name, int value) {
- assert name != null;
- return attribute(name, String.valueOf(value));
- }
- public XmlStringBuilder attribute(String name, long value) {
- assert name != null;
- return attribute(name, String.valueOf(value));
- }
- public XmlStringBuilder jidAttribute(Jid jid) {
- assert jid != null;
- return attribute("jid", jid);
- }
- public XmlStringBuilder optJidAttribute(Jid jid) {
- if (jid != null) {
- attribute("jid", jid);
- }
- return this;
- }
- public XmlStringBuilder optAttribute(String name, String value) {
- if (value != null) {
- attribute(name, value);
- }
- return this;
- }
- public XmlStringBuilder optAttribute(String name, Long value) {
- if (value != null) {
- attribute(name, value);
- }
- return this;
- }
- /**
- * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
- * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
- * if {@link java.util.Date} instance is not <code>null</code>.
- *
- * @param name attribute name
- * @param value value of this attribute
- * @return this XmlStringBuilder
- */
- public XmlStringBuilder optAttribute(String name, Date value) {
- if (value != null) {
- attribute(name, value);
- }
- return this;
- }
- public XmlStringBuilder optAttribute(String name, CharSequence value) {
- if (value != null) {
- attribute(name, value.toString());
- }
- return this;
- }
- public XmlStringBuilder optAttribute(String name, Enum<?> value) {
- if (value != null) {
- attribute(name, value.toString());
- }
- return this;
- }
- public XmlStringBuilder optAttribute(String name, Number number) {
- if (number != null) {
- attribute(name, number.toString());
- }
- return this;
- }
- /**
- * Same as {@link #optAttribute(String, CharSequence)}, but with a different method name. This method can be used if
- * the provided attribute value argument type causes ambiguity in method overloading. For example if the type is a
- * subclass of Number and CharSequence.
- *
- * @param name the name of the attribute.
- * @param value the value of the attribute.
- * @return a reference to this object.
- * @since 4.5
- */
- public XmlStringBuilder optAttributeCs(String name, CharSequence value) {
- return optAttribute(name, value);
- }
- /**
- * Add the given attribute if {@code value => 0}.
- *
- * @param name TODO javadoc me please
- * @param value TODO javadoc me please
- * @return a reference to this object
- */
- public XmlStringBuilder optIntAttribute(String name, int value) {
- if (value >= 0) {
- attribute(name, Integer.toString(value));
- }
- return this;
- }
- /**
- * If the provided Integer argument is not null, then add a new XML attribute with the given name and the Integer as
- * value.
- *
- * @param name the XML attribute name.
- * @param value the optional integer to use as the attribute's value.
- * @return a reference to this object.
- * @since 4.4.1
- */
- public XmlStringBuilder optIntAttribute(String name, Integer value) {
- if (value != null) {
- attribute(name, value.toString());
- }
- return this;
- }
- /**
- * Add the given attribute if value not null and {@code value => 0}.
- *
- * @param name TODO javadoc me please
- * @param value TODO javadoc me please
- * @return a reference to this object
- */
- public XmlStringBuilder optLongAttribute(String name, Long value) {
- if (value != null && value >= 0) {
- attribute(name, Long.toString(value));
- }
- return this;
- }
- public XmlStringBuilder optBooleanAttribute(String name, boolean bool) {
- if (bool) {
- sb.append(' ').append(name).append("='true'");
- }
- return this;
- }
- public XmlStringBuilder optBooleanAttributeDefaultTrue(String name, boolean bool) {
- if (!bool) {
- sb.append(' ').append(name).append("='false'");
- }
- return this;
- }
- private static final class XmlNsAttribute implements CharSequence {
- private final String value;
- private final String xmlFragment;
- private XmlNsAttribute(String value) {
- this.value = StringUtils.requireNotNullNorEmpty(value, "Value must not be null");
- this.xmlFragment = " xmlns='" + value + '\'';
- }
- @Override
- public String toString() {
- return xmlFragment;
- }
- @Override
- public int length() {
- return xmlFragment.length();
- }
- @Override
- public char charAt(int index) {
- return xmlFragment.charAt(index);
- }
- @Override
- public CharSequence subSequence(int start, int end) {
- return xmlFragment.subSequence(start, end);
- }
- }
- public XmlStringBuilder xmlnsAttribute(String value) {
- if (value == null || (effectiveXmlEnvironment != null
- && effectiveXmlEnvironment.effectiveNamespaceEquals(value))) {
- return this;
- }
- XmlNsAttribute xmlNsAttribute = new XmlNsAttribute(value);
- append(xmlNsAttribute);
- return this;
- }
- public XmlStringBuilder xmllangAttribute(String value) {
- // TODO: This should probably be attribute(), not optAttribute().
- optAttribute("xml:lang", value);
- return this;
- }
- public XmlStringBuilder optXmlLangAttribute(String lang) {
- if (!StringUtils.isNullOrEmpty(lang)) {
- xmllangAttribute(lang);
- }
- return this;
- }
- public XmlStringBuilder text(CharSequence text) {
- assert text != null;
- CharSequence escapedText = StringUtils.escapeForXmlText(text);
- sb.append(escapedText);
- return this;
- }
- public XmlStringBuilder escape(String text) {
- assert text != null;
- sb.append(StringUtils.escapeForXml(text));
- return this;
- }
- public XmlStringBuilder escapeAttributeValue(String value) {
- assert value != null;
- sb.append(StringUtils.escapeForXmlAttributeApos(value));
- return this;
- }
- public XmlStringBuilder optEscape(CharSequence text) {
- if (text == null) {
- return this;
- }
- return escape(text);
- }
- public XmlStringBuilder escape(CharSequence text) {
- return escape(text.toString());
- }
- protected XmlStringBuilder prelude(XmlElement pe) {
- return prelude(pe.getElementName(), pe.getNamespace());
- }
- protected XmlStringBuilder prelude(String elementName, String namespace) {
- halfOpenElement(elementName);
- xmlnsAttribute(namespace);
- return this;
- }
- public XmlStringBuilder optAppend(Element element) {
- if (element != null) {
- append(element.toXML(effectiveXmlEnvironment));
- }
- return this;
- }
- public XmlStringBuilder optAppend(Collection<? extends Element> elements) {
- if (elements != null) {
- append(elements);
- }
- return this;
- }
- public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) {
- if (sqc == null) {
- return closeEmptyElement();
- }
- rightAngleBracket();
- escape(sqc);
- closeElement(parentElement);
- return this;
- }
- public XmlStringBuilder append(XmlStringBuilder xsb) {
- assert xsb != null;
- sb.append(xsb.sb);
- return this;
- }
- public XmlStringBuilder append(Element element) {
- return append(element.toXML(effectiveXmlEnvironment));
- }
- public XmlStringBuilder append(Collection<? extends Element> elements) {
- for (Element element : elements) {
- append(element);
- }
- return this;
- }
- public XmlStringBuilder emptyElement(Enum<?> element) {
- // Use Enum.toString() instead Enum.name() here, since some enums override toString() in order to replace
- // underscores ('_') with dash ('-') for example (name() is declared final in Enum).
- return emptyElement(element.toString());
- }
- public XmlStringBuilder emptyElement(String element) {
- halfOpenElement(element);
- return closeEmptyElement();
- }
- public XmlStringBuilder condEmptyElement(boolean condition, String element) {
- if (condition) {
- emptyElement(element);
- }
- return this;
- }
- public XmlStringBuilder condAttribute(boolean condition, String name, String value) {
- if (condition) {
- attribute(name, value);
- }
- return this;
- }
- enum AppendApproach {
- /**
- * Simply add the given CharSequence to this builder.
- */
- SINGLE,
- /**
- * If the given CharSequence is a {@link XmlStringBuilder} or {@link LazyStringBuilder}, then copy the
- * references of the lazy strings parts into this builder. This approach flattens the string builders into one,
- * yielding a different performance characteristic.
- */
- FLAT,
- }
- private static AppendApproach APPEND_APPROACH = AppendApproach.SINGLE;
- /**
- * Set the builders approach on how to append new char sequences.
- *
- * @param appendApproach the append approach.
- */
- public static void setAppendMethod(AppendApproach appendApproach) {
- Objects.requireNonNull(appendApproach);
- APPEND_APPROACH = appendApproach;
- }
- @Override
- public XmlStringBuilder append(CharSequence csq) {
- assert csq != null;
- switch (APPEND_APPROACH) {
- case SINGLE:
- sb.append(csq);
- break;
- case FLAT:
- if (csq instanceof XmlStringBuilder) {
- sb.append(((XmlStringBuilder) csq).sb);
- } else if (csq instanceof LazyStringBuilder) {
- sb.append((LazyStringBuilder) csq);
- } else {
- sb.append(csq);
- }
- break;
- }
- return this;
- }
- @Override
- public XmlStringBuilder append(CharSequence csq, int start, int end) {
- assert csq != null;
- sb.append(csq, start, end);
- return this;
- }
- @Override
- public XmlStringBuilder append(char c) {
- sb.append(c);
- return this;
- }
- @Override
- public int length() {
- return sb.length();
- }
- @Override
- public char charAt(int index) {
- return sb.charAt(index);
- }
- @Override
- public CharSequence subSequence(int start, int end) {
- return sb.subSequence(start, end);
- }
- @Override
- public String toString() {
- return sb.toString();
- }
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof CharSequence)) {
- return false;
- }
- CharSequence otherCharSequenceBuilder = (CharSequence) other;
- return toString().equals(otherCharSequenceBuilder.toString());
- }
- @Override
- public int hashCode() {
- return toString().hashCode();
- }
- private static final class WrappedIoException extends RuntimeException {
- private static final long serialVersionUID = 1L;
- private final IOException wrappedIoException;
- private WrappedIoException(IOException wrappedIoException) {
- this.wrappedIoException = wrappedIoException;
- }
- }
- /**
- * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write
- * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the
- * XmlStringBuilder contents.
- *
- * @param writer TODO javadoc me please
- * @param enclosingXmlEnvironment the enclosing XML environment.
- * @throws IOException if an I/O error occurred.
- */
- public void write(Writer writer, XmlEnvironment enclosingXmlEnvironment) throws IOException {
- try {
- appendXmlTo(csq -> {
- try {
- writer.append(csq);
- } catch (IOException e) {
- throw new WrappedIoException(e);
- }
- }, enclosingXmlEnvironment);
- } catch (WrappedIoException e) {
- throw e.wrappedIoException;
- }
- }
- public List<CharSequence> toList(XmlEnvironment enclosingXmlEnvironment) {
- List<CharSequence> res = new ArrayList<>(sb.getAsList().size());
- appendXmlTo(csq -> res.add(csq), enclosingXmlEnvironment);
- return res;
- }
- @Override
- public StringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) {
- // This is only the potential length, since the actual length depends on the given XmlEnvironment.
- int potentialLength = length();
- StringBuilder res = new StringBuilder(potentialLength);
- appendXmlTo(csq -> res.append(csq), enclosingXmlEnvironment);
- return res;
- }
- private void appendXmlTo(Consumer<CharSequence> charSequenceSink, XmlEnvironment enclosingXmlEnvironment) {
- for (CharSequence csq : sb.getAsList()) {
- if (csq instanceof XmlStringBuilder) {
- ((XmlStringBuilder) csq).appendXmlTo(charSequenceSink, enclosingXmlEnvironment);
- }
- else if (csq instanceof XmlNsAttribute) {
- XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq;
- if (!xmlNsAttribute.value.equals(enclosingXmlEnvironment.getEffectiveNamespace())) {
- charSequenceSink.accept(xmlNsAttribute);
- enclosingXmlEnvironment = new XmlEnvironment(xmlNsAttribute.value);
- }
- }
- else {
- charSequenceSink.accept(csq);
- }
- }
- }
- }