XmlStringBuilder.java

  1. /**
  2.  *
  3.  * Copyright 2014-2024 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smack.util;

  18. import java.io.IOException;
  19. import java.io.Writer;
  20. import java.util.ArrayList;
  21. import java.util.Collection;
  22. import java.util.Date;
  23. import java.util.List;

  24. import org.jivesoftware.smack.packet.Element;
  25. import org.jivesoftware.smack.packet.NamedElement;
  26. import org.jivesoftware.smack.packet.XmlElement;
  27. import org.jivesoftware.smack.packet.XmlEnvironment;

  28. import org.jxmpp.jid.Jid;
  29. import org.jxmpp.util.XmppDateTime;

  30. public class XmlStringBuilder implements Appendable, CharSequence, Element {
  31.     public static final String RIGHT_ANGLE_BRACKET = Character.toString('>');

  32.     private final LazyStringBuilder sb;

  33.     private final XmlEnvironment effectiveXmlEnvironment;

  34.     public XmlStringBuilder() {
  35.         sb = new LazyStringBuilder();
  36.         effectiveXmlEnvironment = null;
  37.     }

  38.     public XmlStringBuilder(XmlElement pe) {
  39.         this(pe, null);
  40.     }

  41.     public XmlStringBuilder(NamedElement e) {
  42.         this();
  43.         halfOpenElement(e.getElementName());
  44.     }

  45.     public XmlStringBuilder(XmlElement element, XmlEnvironment enclosingXmlEnvironment) {
  46.         this(element.getElementName(), element.getNamespace(), element.getLanguage(), enclosingXmlEnvironment);
  47.     }

  48.     public XmlStringBuilder(String elementName, String xmlNs, String xmlLang, XmlEnvironment enclosingXmlEnvironment) {
  49.         sb = new LazyStringBuilder();
  50.         halfOpenElement(elementName);

  51.         if (enclosingXmlEnvironment == null) {
  52.             xmlnsAttribute(xmlNs);
  53.             xmllangAttribute(xmlLang);
  54.         } else {
  55.             if (!enclosingXmlEnvironment.effectiveNamespaceEquals(xmlNs)) {
  56.                 xmlnsAttribute(xmlNs);
  57.             }
  58.             if (!enclosingXmlEnvironment.effectiveLanguageEquals(xmlLang)) {
  59.                 xmllangAttribute(xmlLang);
  60.             }
  61.         }

  62.         effectiveXmlEnvironment = XmlEnvironment.builder()
  63.                 .withNamespace(xmlNs)
  64.                 .withLanguage(xmlLang)
  65.                 .withNext(enclosingXmlEnvironment)
  66.                 .build();
  67.     }

  68.     public XmlEnvironment getXmlEnvironment() {
  69.         return effectiveXmlEnvironment;
  70.     }

  71.     public XmlStringBuilder escapedElement(String name, String escapedContent) {
  72.         assert escapedContent != null;
  73.         openElement(name);
  74.         append(escapedContent);
  75.         closeElement(name);
  76.         return this;
  77.     }

  78.     /**
  79.      * Add a new element to this builder.
  80.      *
  81.      * @param name TODO javadoc me please
  82.      * @param content TODO javadoc me please
  83.      * @return the XmlStringBuilder
  84.      */
  85.     public XmlStringBuilder element(String name, String content) {
  86.         if (content.isEmpty()) {
  87.             return emptyElement(name);
  88.         }
  89.         openElement(name);
  90.         escape(content);
  91.         closeElement(name);
  92.         return this;
  93.     }

  94.     /**
  95.      * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
  96.      * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
  97.      *
  98.      * @param name element name
  99.      * @param content content of element
  100.      * @return this XmlStringBuilder
  101.      */
  102.     public XmlStringBuilder element(String name, Date content) {
  103.         assert content != null;
  104.         return element(name, XmppDateTime.formatXEP0082Date(content));
  105.     }

  106.    /**
  107.     * Add a new element to this builder.
  108.     *
  109.     * @param name TODO javadoc me please
  110.     * @param content TODO javadoc me please
  111.     * @return the XmlStringBuilder
  112.     */
  113.    public XmlStringBuilder element(String name, CharSequence content) {
  114.        return element(name, content.toString());
  115.    }

  116.     public XmlStringBuilder element(String name, Enum<?> content) {
  117.         assert content != null;
  118.         element(name, content.toString());
  119.         return this;
  120.     }

  121.     public XmlStringBuilder optElement(String name, String content) {
  122.         if (content != null) {
  123.             element(name, content);
  124.         }
  125.         return this;
  126.     }

  127.     /**
  128.      * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
  129.      * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
  130.      * if {@link java.util.Date} instance is not <code>null</code>.
  131.      *
  132.      * @param name element name
  133.      * @param content content of element
  134.      * @return this XmlStringBuilder
  135.      */
  136.     public XmlStringBuilder optElement(String name, Date content) {
  137.         if (content != null) {
  138.             element(name, content);
  139.         }
  140.         return this;
  141.     }

  142.     public XmlStringBuilder optElement(String name, CharSequence content) {
  143.         if (content != null) {
  144.             element(name, content.toString());
  145.         }
  146.         return this;
  147.     }

  148.     public XmlStringBuilder optElement(Element element) {
  149.         if (element != null) {
  150.             append(element);
  151.         }
  152.         return this;
  153.     }

  154.     public XmlStringBuilder optElement(String name, Enum<?> content) {
  155.         if (content != null) {
  156.             element(name, content);
  157.         }
  158.         return this;
  159.     }

  160.     public XmlStringBuilder optElement(String name, Object object) {
  161.         if (object != null) {
  162.             element(name, object.toString());
  163.         }
  164.         return this;
  165.     }

  166.     public XmlStringBuilder optIntElement(String name, int value) {
  167.         if (value >= 0) {
  168.             element(name, String.valueOf(value));
  169.         }
  170.         return this;
  171.     }

  172.     public XmlStringBuilder halfOpenElement(String name) {
  173.         assert StringUtils.isNotEmpty(name);
  174.         sb.append('<').append(name);
  175.         return this;
  176.     }

  177.     public XmlStringBuilder halfOpenElement(NamedElement namedElement) {
  178.         return halfOpenElement(namedElement.getElementName());
  179.     }

  180.     public XmlStringBuilder openElement(String name) {
  181.         halfOpenElement(name).rightAngleBracket();
  182.         return this;
  183.     }

  184.     public XmlStringBuilder closeElement(String name) {
  185.         sb.append("</").append(name);
  186.         rightAngleBracket();
  187.         return this;
  188.     }

  189.     public XmlStringBuilder closeElement(NamedElement e) {
  190.         closeElement(e.getElementName());
  191.         return this;
  192.     }

  193.     public XmlStringBuilder closeEmptyElement() {
  194.         sb.append("/>");
  195.         return this;
  196.     }

  197.     /**
  198.      * Add a right angle bracket '&gt;'.
  199.      *
  200.      * @return a reference to this object.
  201.      */
  202.     public XmlStringBuilder rightAngleBracket() {
  203.         sb.append(RIGHT_ANGLE_BRACKET);
  204.         return this;
  205.     }

  206.     /**
  207.      * Does nothing if value is null.
  208.      *
  209.      * @param name TODO javadoc me please
  210.      * @param value TODO javadoc me please
  211.      * @return the XmlStringBuilder
  212.      */
  213.     public XmlStringBuilder attribute(String name, String value) {
  214.         assert value != null;
  215.         sb.append(' ').append(name).append("='");
  216.         escapeAttributeValue(value);
  217.         sb.append('\'');
  218.         return this;
  219.     }

  220.     public XmlStringBuilder attribute(String name, boolean bool) {
  221.         return attribute(name, Boolean.toString(bool));
  222.     }

  223.     /**
  224.      * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
  225.      * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
  226.      *
  227.      * @param name name of attribute
  228.      * @param value value of attribute
  229.      * @return this XmlStringBuilder
  230.      */
  231.     public XmlStringBuilder attribute(String name, Date value) {
  232.         assert value != null;
  233.         return attribute(name, XmppDateTime.formatXEP0082Date(value));
  234.     }

  235.     public XmlStringBuilder attribute(String name, CharSequence value) {
  236.         return attribute(name, value.toString());
  237.     }

  238.     public XmlStringBuilder attribute(String name, Enum<?> value) {
  239.         assert value != null;
  240.         attribute(name, value.toString());
  241.         return this;
  242.     }

  243.     public <E extends Enum<?>> XmlStringBuilder attribute(String name, E value, E implicitDefault) {
  244.         if (value == null || value == implicitDefault) {
  245.             return this;
  246.         }

  247.         attribute(name, value.toString());
  248.         return this;
  249.     }

  250.     public XmlStringBuilder attribute(String name, int value) {
  251.         assert name != null;
  252.         return attribute(name, String.valueOf(value));
  253.     }

  254.     public XmlStringBuilder attribute(String name, long value) {
  255.         assert name != null;
  256.         return attribute(name, String.valueOf(value));
  257.     }

  258.     public XmlStringBuilder jidAttribute(Jid jid) {
  259.         assert jid != null;
  260.         return attribute("jid", jid);
  261.     }

  262.     public XmlStringBuilder optJidAttribute(Jid jid) {
  263.         if (jid != null) {
  264.             attribute("jid", jid);
  265.         }
  266.         return this;
  267.     }

  268.     public XmlStringBuilder optAttribute(String name, String value) {
  269.         if (value != null) {
  270.             attribute(name, value);
  271.         }
  272.         return this;
  273.     }

  274.     public XmlStringBuilder optAttribute(String name, Long value) {
  275.         if (value != null) {
  276.             attribute(name, value);
  277.         }
  278.         return this;
  279.     }

  280.     /**
  281.      * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
  282.      * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
  283.      * if {@link java.util.Date} instance is not <code>null</code>.
  284.      *
  285.      * @param name attribute name
  286.      * @param value value of this attribute
  287.      * @return this XmlStringBuilder
  288.      */
  289.     public XmlStringBuilder optAttribute(String name, Date value) {
  290.         if (value != null) {
  291.             attribute(name, value);
  292.         }
  293.         return this;
  294.     }

  295.     public XmlStringBuilder optAttribute(String name, CharSequence value) {
  296.         if (value != null) {
  297.             attribute(name, value.toString());
  298.         }
  299.         return this;
  300.     }

  301.     public XmlStringBuilder optAttribute(String name, Enum<?> value) {
  302.         if (value != null) {
  303.             attribute(name, value.toString());
  304.         }
  305.         return this;
  306.     }

  307.     public XmlStringBuilder optAttribute(String name, Number number) {
  308.         if (number != null) {
  309.             attribute(name, number.toString());
  310.         }
  311.         return this;
  312.     }

  313.     /**
  314.      * Same as {@link #optAttribute(String, CharSequence)}, but with a different method name. This method can be used if
  315.      * the provided attribute value argument type causes ambiguity in method overloading. For example if the type is a
  316.      * subclass of Number and CharSequence.
  317.      *
  318.      * @param name the name of the attribute.
  319.      * @param value the value of the attribute.
  320.      * @return a reference to this object.
  321.      * @since 4.5
  322.      */
  323.     public XmlStringBuilder optAttributeCs(String name, CharSequence value) {
  324.         return optAttribute(name, value);
  325.     }

  326.     /**
  327.      * Add the given attribute if {@code value => 0}.
  328.      *
  329.      * @param name TODO javadoc me please
  330.      * @param value TODO javadoc me please
  331.      * @return a reference to this object
  332.      */
  333.     public XmlStringBuilder optIntAttribute(String name, int value) {
  334.         if (value >= 0) {
  335.             attribute(name, Integer.toString(value));
  336.         }
  337.         return this;
  338.     }

  339.     /**
  340.      * If the provided Integer argument is not null, then add a new XML attribute with the given name and the Integer as
  341.      * value.
  342.      *
  343.      * @param name the XML attribute name.
  344.      * @param value the optional integer to use as the attribute's value.
  345.      * @return a reference to this object.
  346.      * @since 4.4.1
  347.      */
  348.     public XmlStringBuilder optIntAttribute(String name, Integer value) {
  349.         if (value != null) {
  350.             attribute(name, value.toString());
  351.         }
  352.         return this;
  353.     }

  354.     /**
  355.      * Add the given attribute if value not null and {@code value => 0}.
  356.      *
  357.      * @param name TODO javadoc me please
  358.      * @param value TODO javadoc me please
  359.      * @return a reference to this object
  360.      */
  361.     public XmlStringBuilder optLongAttribute(String name, Long value) {
  362.         if (value != null && value >= 0) {
  363.             attribute(name, Long.toString(value));
  364.         }
  365.         return this;
  366.     }

  367.     public XmlStringBuilder optBooleanAttribute(String name, boolean bool) {
  368.         if (bool) {
  369.             sb.append(' ').append(name).append("='true'");
  370.         }
  371.         return this;
  372.     }

  373.     public XmlStringBuilder optBooleanAttributeDefaultTrue(String name, boolean bool) {
  374.         if (!bool) {
  375.             sb.append(' ').append(name).append("='false'");
  376.         }
  377.         return this;
  378.     }

  379.     private static final class XmlNsAttribute implements CharSequence {
  380.         private final String value;
  381.         private final String xmlFragment;

  382.         private XmlNsAttribute(String value) {
  383.             this.value = StringUtils.requireNotNullNorEmpty(value, "Value must not be null");
  384.             this.xmlFragment = " xmlns='" + value + '\'';
  385.         }

  386.         @Override
  387.         public String toString() {
  388.             return xmlFragment;
  389.         }

  390.         @Override
  391.         public int length() {
  392.             return xmlFragment.length();
  393.         }

  394.         @Override
  395.         public char charAt(int index) {
  396.             return xmlFragment.charAt(index);
  397.         }

  398.         @Override
  399.         public CharSequence subSequence(int start, int end) {
  400.             return xmlFragment.subSequence(start, end);
  401.         }
  402.     }

  403.     public XmlStringBuilder xmlnsAttribute(String value) {
  404.         if (value == null || (effectiveXmlEnvironment != null
  405.                         && effectiveXmlEnvironment.effectiveNamespaceEquals(value))) {
  406.             return this;
  407.         }
  408.         XmlNsAttribute xmlNsAttribute = new XmlNsAttribute(value);
  409.         append(xmlNsAttribute);
  410.         return this;
  411.     }

  412.     public XmlStringBuilder xmllangAttribute(String value) {
  413.         // TODO: This should probably be attribute(), not optAttribute().
  414.         optAttribute("xml:lang", value);
  415.         return this;
  416.     }

  417.     public XmlStringBuilder optXmlLangAttribute(String lang) {
  418.         if (!StringUtils.isNullOrEmpty(lang)) {
  419.             xmllangAttribute(lang);
  420.         }
  421.         return this;
  422.     }

  423.     public XmlStringBuilder text(CharSequence text) {
  424.         assert text != null;
  425.         CharSequence escapedText = StringUtils.escapeForXmlText(text);
  426.         sb.append(escapedText);
  427.         return this;
  428.     }

  429.     public XmlStringBuilder escape(String text) {
  430.         assert text != null;
  431.         sb.append(StringUtils.escapeForXml(text));
  432.         return this;
  433.     }

  434.     public XmlStringBuilder escapeAttributeValue(String value) {
  435.         assert value != null;
  436.         sb.append(StringUtils.escapeForXmlAttributeApos(value));
  437.         return this;
  438.     }

  439.     public XmlStringBuilder optEscape(CharSequence text) {
  440.         if (text == null) {
  441.             return this;
  442.         }
  443.         return escape(text);
  444.     }

  445.     public XmlStringBuilder escape(CharSequence text) {
  446.         return escape(text.toString());
  447.     }

  448.     protected XmlStringBuilder prelude(XmlElement pe) {
  449.         return prelude(pe.getElementName(), pe.getNamespace());
  450.     }

  451.     protected XmlStringBuilder prelude(String elementName, String namespace) {
  452.         halfOpenElement(elementName);
  453.         xmlnsAttribute(namespace);
  454.         return this;
  455.     }

  456.     public XmlStringBuilder optAppend(Element element) {
  457.         if (element != null) {
  458.             append(element.toXML(effectiveXmlEnvironment));
  459.         }
  460.         return this;
  461.     }

  462.     public XmlStringBuilder optAppend(Collection<? extends Element> elements) {
  463.         if (elements != null) {
  464.             append(elements);
  465.         }
  466.         return this;
  467.     }

  468.     public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) {
  469.         if (sqc == null) {
  470.             return closeEmptyElement();
  471.         }
  472.         rightAngleBracket();
  473.         escape(sqc);
  474.         closeElement(parentElement);
  475.         return this;
  476.     }

  477.     public XmlStringBuilder append(XmlStringBuilder xsb) {
  478.         assert xsb != null;
  479.         sb.append(xsb.sb);
  480.         return this;
  481.     }

  482.     public XmlStringBuilder append(Element element) {
  483.         return append(element.toXML(effectiveXmlEnvironment));
  484.     }

  485.     public XmlStringBuilder append(Collection<? extends Element> elements) {
  486.         for (Element element : elements) {
  487.             append(element);
  488.         }
  489.         return this;
  490.     }

  491.     public XmlStringBuilder emptyElement(Enum<?> element) {
  492.         // Use Enum.toString() instead Enum.name() here, since some enums override toString() in order to replace
  493.         // underscores ('_') with dash ('-') for example (name() is declared final in Enum).
  494.         return emptyElement(element.toString());
  495.     }

  496.     public XmlStringBuilder emptyElement(String element) {
  497.         halfOpenElement(element);
  498.         return closeEmptyElement();
  499.     }

  500.     public XmlStringBuilder condEmptyElement(boolean condition, String element) {
  501.         if (condition) {
  502.             emptyElement(element);
  503.         }
  504.         return this;
  505.     }

  506.     public XmlStringBuilder condAttribute(boolean condition, String name, String value) {
  507.         if (condition) {
  508.             attribute(name, value);
  509.         }
  510.         return this;
  511.     }

  512.     enum AppendApproach {
  513.         /**
  514.          * Simply add the given CharSequence to this builder.
  515.          */
  516.         SINGLE,

  517.         /**
  518.          * If the given CharSequence is a {@link XmlStringBuilder} or {@link LazyStringBuilder}, then copy the
  519.          * references of the lazy strings parts into this builder. This approach flattens the string builders into one,
  520.          * yielding a different performance characteristic.
  521.          */
  522.         FLAT,
  523.     }

  524.     private static AppendApproach APPEND_APPROACH = AppendApproach.SINGLE;

  525.     /**
  526.      * Set the builders approach on how to append new char sequences.
  527.      *
  528.      * @param appendApproach the append approach.
  529.      */
  530.     public static void setAppendMethod(AppendApproach appendApproach) {
  531.         Objects.requireNonNull(appendApproach);
  532.         APPEND_APPROACH = appendApproach;
  533.     }

  534.     @Override
  535.     public XmlStringBuilder append(CharSequence csq) {
  536.         assert csq != null;
  537.         switch (APPEND_APPROACH) {
  538.         case SINGLE:
  539.             sb.append(csq);
  540.             break;
  541.         case FLAT:
  542.             if (csq instanceof XmlStringBuilder) {
  543.                 sb.append(((XmlStringBuilder) csq).sb);
  544.             } else if (csq instanceof LazyStringBuilder) {
  545.                 sb.append((LazyStringBuilder) csq);
  546.             } else {
  547.                 sb.append(csq);
  548.             }
  549.             break;
  550.         }
  551.         return this;
  552.     }

  553.     @Override
  554.     public XmlStringBuilder append(CharSequence csq, int start, int end) {
  555.         assert csq != null;
  556.         sb.append(csq, start, end);
  557.         return this;
  558.     }

  559.     @Override
  560.     public XmlStringBuilder append(char c) {
  561.         sb.append(c);
  562.         return this;
  563.     }

  564.     @Override
  565.     public int length() {
  566.         return sb.length();
  567.     }

  568.     @Override
  569.     public char charAt(int index) {
  570.         return sb.charAt(index);
  571.     }

  572.     @Override
  573.     public CharSequence subSequence(int start, int end) {
  574.         return sb.subSequence(start, end);
  575.     }

  576.     @Override
  577.     public String toString() {
  578.         return sb.toString();
  579.     }

  580.     @Override
  581.     public boolean equals(Object other) {
  582.         if (!(other instanceof CharSequence)) {
  583.             return false;
  584.         }
  585.         CharSequence otherCharSequenceBuilder = (CharSequence) other;
  586.         return toString().equals(otherCharSequenceBuilder.toString());
  587.     }

  588.     @Override
  589.     public int hashCode() {
  590.         return toString().hashCode();
  591.     }

  592.     private static final class WrappedIoException extends RuntimeException {

  593.         private static final long serialVersionUID = 1L;

  594.         private final IOException wrappedIoException;

  595.         private WrappedIoException(IOException wrappedIoException) {
  596.             this.wrappedIoException = wrappedIoException;
  597.         }
  598.     }

  599.     /**
  600.      * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write
  601.      * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the
  602.      * XmlStringBuilder contents.
  603.      *
  604.      * @param writer TODO javadoc me please
  605.      * @param enclosingXmlEnvironment the enclosing XML environment.
  606.      * @throws IOException if an I/O error occurred.
  607.      */
  608.     public void write(Writer writer, XmlEnvironment enclosingXmlEnvironment) throws IOException {
  609.         try {
  610.             appendXmlTo(csq -> {
  611.                 try {
  612.                     writer.append(csq);
  613.                 } catch (IOException e) {
  614.                     throw new WrappedIoException(e);
  615.                 }
  616.             }, enclosingXmlEnvironment);
  617.         } catch (WrappedIoException e) {
  618.             throw e.wrappedIoException;
  619.         }
  620.     }

  621.     public List<CharSequence> toList(XmlEnvironment enclosingXmlEnvironment) {
  622.         List<CharSequence> res = new ArrayList<>(sb.getAsList().size());

  623.         appendXmlTo(csq -> res.add(csq), enclosingXmlEnvironment);

  624.         return res;
  625.     }

  626.     @Override
  627.     public StringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) {
  628.         // This is only the potential length, since the actual length depends on the given XmlEnvironment.
  629.         int potentialLength = length();
  630.         StringBuilder res = new StringBuilder(potentialLength);

  631.         appendXmlTo(csq -> res.append(csq), enclosingXmlEnvironment);

  632.         return res;
  633.     }

  634.     private void appendXmlTo(Consumer<CharSequence> charSequenceSink, XmlEnvironment enclosingXmlEnvironment) {
  635.         for (CharSequence csq : sb.getAsList()) {
  636.             if (csq instanceof XmlStringBuilder) {
  637.                 ((XmlStringBuilder) csq).appendXmlTo(charSequenceSink, enclosingXmlEnvironment);
  638.             }
  639.             else if (csq instanceof XmlNsAttribute) {
  640.                 XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq;
  641.                 if (!xmlNsAttribute.value.equals(enclosingXmlEnvironment.getEffectiveNamespace())) {
  642.                     charSequenceSink.accept(xmlNsAttribute);
  643.                     enclosingXmlEnvironment = new XmlEnvironment(xmlNsAttribute.value);
  644.                 }
  645.             }
  646.             else {
  647.                 charSequenceSink.accept(csq);
  648.             }
  649.         }
  650.     }
  651. }