001/**
002 *
003 * Copyright 2014-2024 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.NamedElement;
028import org.jivesoftware.smack.packet.XmlElement;
029import org.jivesoftware.smack.packet.XmlEnvironment;
030
031import org.jxmpp.jid.Jid;
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(XmlElement pe) {
047        this(pe, null);
048    }
049
050    public XmlStringBuilder(NamedElement e) {
051        this();
052        halfOpenElement(e.getElementName());
053    }
054
055    public XmlStringBuilder(XmlElement element, XmlEnvironment enclosingXmlEnvironment) {
056        this(element.getElementName(), element.getNamespace(), element.getLanguage(), enclosingXmlEnvironment);
057    }
058
059    public XmlStringBuilder(String elementName, String xmlNs, String xmlLang, XmlEnvironment enclosingXmlEnvironment) {
060        sb = new LazyStringBuilder();
061        halfOpenElement(elementName);
062
063        if (enclosingXmlEnvironment == null) {
064            xmlnsAttribute(xmlNs);
065            xmllangAttribute(xmlLang);
066        } else {
067            if (!enclosingXmlEnvironment.effectiveNamespaceEquals(xmlNs)) {
068                xmlnsAttribute(xmlNs);
069            }
070            if (!enclosingXmlEnvironment.effectiveLanguageEquals(xmlLang)) {
071                xmllangAttribute(xmlLang);
072            }
073        }
074
075        effectiveXmlEnvironment = XmlEnvironment.builder()
076                .withNamespace(xmlNs)
077                .withLanguage(xmlLang)
078                .withNext(enclosingXmlEnvironment)
079                .build();
080    }
081
082    public XmlEnvironment getXmlEnvironment() {
083        return effectiveXmlEnvironment;
084    }
085
086    public XmlStringBuilder escapedElement(String name, String escapedContent) {
087        assert escapedContent != null;
088        openElement(name);
089        append(escapedContent);
090        closeElement(name);
091        return this;
092    }
093
094    /**
095     * Add a new element to this builder.
096     *
097     * @param name TODO javadoc me please
098     * @param content TODO javadoc me please
099     * @return the XmlStringBuilder
100     */
101    public XmlStringBuilder element(String name, String content) {
102        if (content.isEmpty()) {
103            return emptyElement(name);
104        }
105        openElement(name);
106        escape(content);
107        closeElement(name);
108        return this;
109    }
110
111    /**
112     * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
113     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
114     *
115     * @param name element name
116     * @param content content of element
117     * @return this XmlStringBuilder
118     */
119    public XmlStringBuilder element(String name, Date content) {
120        assert content != null;
121        return element(name, XmppDateTime.formatXEP0082Date(content));
122    }
123
124   /**
125    * Add a new element to this builder.
126    *
127    * @param name TODO javadoc me please
128    * @param content TODO javadoc me please
129    * @return the XmlStringBuilder
130    */
131   public XmlStringBuilder element(String name, CharSequence content) {
132       return element(name, content.toString());
133   }
134
135    public XmlStringBuilder element(String name, Enum<?> content) {
136        assert content != null;
137        element(name, content.toString());
138        return this;
139    }
140
141    public XmlStringBuilder optElement(String name, String content) {
142        if (content != null) {
143            element(name, content);
144        }
145        return this;
146    }
147
148    /**
149     * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
150     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
151     * if {@link java.util.Date} instance is not <code>null</code>.
152     *
153     * @param name element name
154     * @param content content of element
155     * @return this XmlStringBuilder
156     */
157    public XmlStringBuilder optElement(String name, Date content) {
158        if (content != null) {
159            element(name, content);
160        }
161        return this;
162    }
163
164    public XmlStringBuilder optElement(String name, CharSequence content) {
165        if (content != null) {
166            element(name, content.toString());
167        }
168        return this;
169    }
170
171    public XmlStringBuilder optElement(Element element) {
172        if (element != null) {
173            append(element);
174        }
175        return this;
176    }
177
178    public XmlStringBuilder optElement(String name, Enum<?> content) {
179        if (content != null) {
180            element(name, content);
181        }
182        return this;
183    }
184
185    public XmlStringBuilder optElement(String name, Object object) {
186        if (object != null) {
187            element(name, object.toString());
188        }
189        return this;
190    }
191
192    public XmlStringBuilder optIntElement(String name, int value) {
193        if (value >= 0) {
194            element(name, String.valueOf(value));
195        }
196        return this;
197    }
198
199    public XmlStringBuilder halfOpenElement(String name) {
200        assert StringUtils.isNotEmpty(name);
201        sb.append('<').append(name);
202        return this;
203    }
204
205    public XmlStringBuilder halfOpenElement(NamedElement namedElement) {
206        return halfOpenElement(namedElement.getElementName());
207    }
208
209    public XmlStringBuilder openElement(String name) {
210        halfOpenElement(name).rightAngleBracket();
211        return this;
212    }
213
214    public XmlStringBuilder closeElement(String name) {
215        sb.append("</").append(name);
216        rightAngleBracket();
217        return this;
218    }
219
220    public XmlStringBuilder closeElement(NamedElement e) {
221        closeElement(e.getElementName());
222        return this;
223    }
224
225    public XmlStringBuilder closeEmptyElement() {
226        sb.append("/>");
227        return this;
228    }
229
230    /**
231     * Add a right angle bracket '&gt;'.
232     *
233     * @return a reference to this object.
234     */
235    public XmlStringBuilder rightAngleBracket() {
236        sb.append(RIGHT_ANGLE_BRACKET);
237        return this;
238    }
239
240    /**
241     * Does nothing if value is null.
242     *
243     * @param name TODO javadoc me please
244     * @param value TODO javadoc me please
245     * @return the XmlStringBuilder
246     */
247    public XmlStringBuilder attribute(String name, String value) {
248        assert value != null;
249        sb.append(' ').append(name).append("='");
250        escapeAttributeValue(value);
251        sb.append('\'');
252        return this;
253    }
254
255    public XmlStringBuilder attribute(String name, boolean bool) {
256        return attribute(name, Boolean.toString(bool));
257    }
258
259    /**
260     * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
261     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
262     *
263     * @param name name of attribute
264     * @param value value of attribute
265     * @return this XmlStringBuilder
266     */
267    public XmlStringBuilder attribute(String name, Date value) {
268        assert value != null;
269        return attribute(name, XmppDateTime.formatXEP0082Date(value));
270    }
271
272    public XmlStringBuilder attribute(String name, CharSequence value) {
273        return attribute(name, value.toString());
274    }
275
276    public XmlStringBuilder attribute(String name, Enum<?> value) {
277        assert value != null;
278        attribute(name, value.toString());
279        return this;
280    }
281
282    public <E extends Enum<?>> XmlStringBuilder attribute(String name, E value, E implicitDefault) {
283        if (value == null || value == implicitDefault) {
284            return this;
285        }
286
287        attribute(name, value.toString());
288        return this;
289    }
290
291    public XmlStringBuilder attribute(String name, int value) {
292        assert name != null;
293        return attribute(name, String.valueOf(value));
294    }
295
296    public XmlStringBuilder attribute(String name, long value) {
297        assert name != null;
298        return attribute(name, String.valueOf(value));
299    }
300
301    public XmlStringBuilder jidAttribute(Jid jid) {
302        assert jid != null;
303        return attribute("jid", jid);
304    }
305
306    public XmlStringBuilder optJidAttribute(Jid jid) {
307        if (jid != null) {
308            attribute("jid", jid);
309        }
310        return this;
311    }
312
313    public XmlStringBuilder optAttribute(String name, String value) {
314        if (value != null) {
315            attribute(name, value);
316        }
317        return this;
318    }
319
320    public XmlStringBuilder optAttribute(String name, Long value) {
321        if (value != null) {
322            attribute(name, value);
323        }
324        return this;
325    }
326
327    /**
328     * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
329     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
330     * if {@link java.util.Date} instance is not <code>null</code>.
331     *
332     * @param name attribute name
333     * @param value value of this attribute
334     * @return this XmlStringBuilder
335     */
336    public XmlStringBuilder optAttribute(String name, Date value) {
337        if (value != null) {
338            attribute(name, value);
339        }
340        return this;
341    }
342
343    public XmlStringBuilder optAttribute(String name, CharSequence value) {
344        if (value != null) {
345            attribute(name, value.toString());
346        }
347        return this;
348    }
349
350    public XmlStringBuilder optAttribute(String name, Enum<?> value) {
351        if (value != null) {
352            attribute(name, value.toString());
353        }
354        return this;
355    }
356
357    public XmlStringBuilder optAttribute(String name, Number number) {
358        if (number != null) {
359            attribute(name, number.toString());
360        }
361        return this;
362    }
363
364    /**
365     * Same as {@link #optAttribute(String, CharSequence)}, but with a different method name. This method can be used if
366     * the provided attribute value argument type causes ambiguity in method overloading. For example if the type is a
367     * subclass of Number and CharSequence.
368     *
369     * @param name the name of the attribute.
370     * @param value the value of the attribute.
371     * @return a reference to this object.
372     * @since 4.5
373     */
374    public XmlStringBuilder optAttributeCs(String name, CharSequence value) {
375        return optAttribute(name, value);
376    }
377
378    /**
379     * Add the given attribute if {@code value => 0}.
380     *
381     * @param name TODO javadoc me please
382     * @param value TODO javadoc me please
383     * @return a reference to this object
384     */
385    public XmlStringBuilder optIntAttribute(String name, int value) {
386        if (value >= 0) {
387            attribute(name, Integer.toString(value));
388        }
389        return this;
390    }
391
392    /**
393     * If the provided Integer argument is not null, then add a new XML attribute with the given name and the Integer as
394     * value.
395     *
396     * @param name the XML attribute name.
397     * @param value the optional integer to use as the attribute's value.
398     * @return a reference to this object.
399     * @since 4.4.1
400     */
401    public XmlStringBuilder optIntAttribute(String name, Integer value) {
402        if (value != null) {
403            attribute(name, value.toString());
404        }
405        return this;
406    }
407
408    /**
409     * Add the given attribute if value not null and {@code value => 0}.
410     *
411     * @param name TODO javadoc me please
412     * @param value TODO javadoc me please
413     * @return a reference to this object
414     */
415    public XmlStringBuilder optLongAttribute(String name, Long value) {
416        if (value != null && value >= 0) {
417            attribute(name, Long.toString(value));
418        }
419        return this;
420    }
421
422    public XmlStringBuilder optBooleanAttribute(String name, boolean bool) {
423        if (bool) {
424            sb.append(' ').append(name).append("='true'");
425        }
426        return this;
427    }
428
429    public XmlStringBuilder optBooleanAttributeDefaultTrue(String name, boolean bool) {
430        if (!bool) {
431            sb.append(' ').append(name).append("='false'");
432        }
433        return this;
434    }
435
436    private static final class XmlNsAttribute implements CharSequence {
437        private final String value;
438        private final String xmlFragment;
439
440        private XmlNsAttribute(String value) {
441            this.value = StringUtils.requireNotNullNorEmpty(value, "Value must not be null");
442            this.xmlFragment = " xmlns='" + value + '\'';
443        }
444
445        @Override
446        public String toString() {
447            return xmlFragment;
448        }
449
450        @Override
451        public int length() {
452            return xmlFragment.length();
453        }
454
455        @Override
456        public char charAt(int index) {
457            return xmlFragment.charAt(index);
458        }
459
460        @Override
461        public CharSequence subSequence(int start, int end) {
462            return xmlFragment.subSequence(start, end);
463        }
464    }
465
466    public XmlStringBuilder xmlnsAttribute(String value) {
467        if (value == null || (effectiveXmlEnvironment != null
468                        && effectiveXmlEnvironment.effectiveNamespaceEquals(value))) {
469            return this;
470        }
471        XmlNsAttribute xmlNsAttribute = new XmlNsAttribute(value);
472        append(xmlNsAttribute);
473        return this;
474    }
475
476    public XmlStringBuilder xmllangAttribute(String value) {
477        // TODO: This should probably be attribute(), not optAttribute().
478        optAttribute("xml:lang", value);
479        return this;
480    }
481
482    public XmlStringBuilder optXmlLangAttribute(String lang) {
483        if (!StringUtils.isNullOrEmpty(lang)) {
484            xmllangAttribute(lang);
485        }
486        return this;
487    }
488
489    public XmlStringBuilder text(CharSequence text) {
490        assert text != null;
491        CharSequence escapedText = StringUtils.escapeForXmlText(text);
492        sb.append(escapedText);
493        return this;
494    }
495
496    public XmlStringBuilder escape(String text) {
497        assert text != null;
498        sb.append(StringUtils.escapeForXml(text));
499        return this;
500    }
501
502    public XmlStringBuilder escapeAttributeValue(String value) {
503        assert value != null;
504        sb.append(StringUtils.escapeForXmlAttributeApos(value));
505        return this;
506    }
507
508    public XmlStringBuilder optEscape(CharSequence text) {
509        if (text == null) {
510            return this;
511        }
512        return escape(text);
513    }
514
515    public XmlStringBuilder escape(CharSequence text) {
516        return escape(text.toString());
517    }
518
519    protected XmlStringBuilder prelude(XmlElement pe) {
520        return prelude(pe.getElementName(), pe.getNamespace());
521    }
522
523    protected XmlStringBuilder prelude(String elementName, String namespace) {
524        halfOpenElement(elementName);
525        xmlnsAttribute(namespace);
526        return this;
527    }
528
529    public XmlStringBuilder optAppend(Element element) {
530        if (element != null) {
531            append(element.toXML(effectiveXmlEnvironment));
532        }
533        return this;
534    }
535
536    public XmlStringBuilder optAppend(Collection<? extends Element> elements) {
537        if (elements != null) {
538            append(elements);
539        }
540        return this;
541    }
542
543    public XmlStringBuilder optTextChild(CharSequence sqc, NamedElement parentElement) {
544        if (sqc == null) {
545            return closeEmptyElement();
546        }
547        rightAngleBracket();
548        escape(sqc);
549        closeElement(parentElement);
550        return this;
551    }
552
553    public XmlStringBuilder append(XmlStringBuilder xsb) {
554        assert xsb != null;
555        sb.append(xsb.sb);
556        return this;
557    }
558
559    public XmlStringBuilder append(Element element) {
560        return append(element.toXML(effectiveXmlEnvironment));
561    }
562
563    public XmlStringBuilder append(Collection<? extends Element> elements) {
564        for (Element element : elements) {
565            append(element);
566        }
567        return this;
568    }
569
570    public XmlStringBuilder emptyElement(Enum<?> element) {
571        // Use Enum.toString() instead Enum.name() here, since some enums override toString() in order to replace
572        // underscores ('_') with dash ('-') for example (name() is declared final in Enum).
573        return emptyElement(element.toString());
574    }
575
576    public XmlStringBuilder emptyElement(String element) {
577        halfOpenElement(element);
578        return closeEmptyElement();
579    }
580
581    public XmlStringBuilder condEmptyElement(boolean condition, String element) {
582        if (condition) {
583            emptyElement(element);
584        }
585        return this;
586    }
587
588    public XmlStringBuilder condAttribute(boolean condition, String name, String value) {
589        if (condition) {
590            attribute(name, value);
591        }
592        return this;
593    }
594
595    enum AppendApproach {
596        /**
597         * Simply add the given CharSequence to this builder.
598         */
599        SINGLE,
600
601        /**
602         * If the given CharSequence is a {@link XmlStringBuilder} or {@link LazyStringBuilder}, then copy the
603         * references of the lazy strings parts into this builder. This approach flattens the string builders into one,
604         * yielding a different performance characteristic.
605         */
606        FLAT,
607    }
608
609    private static AppendApproach APPEND_APPROACH = AppendApproach.SINGLE;
610
611    /**
612     * Set the builders approach on how to append new char sequences.
613     *
614     * @param appendApproach the append approach.
615     */
616    public static void setAppendMethod(AppendApproach appendApproach) {
617        Objects.requireNonNull(appendApproach);
618        APPEND_APPROACH = appendApproach;
619    }
620
621    @Override
622    public XmlStringBuilder append(CharSequence csq) {
623        assert csq != null;
624        switch (APPEND_APPROACH) {
625        case SINGLE:
626            sb.append(csq);
627            break;
628        case FLAT:
629            if (csq instanceof XmlStringBuilder) {
630                sb.append(((XmlStringBuilder) csq).sb);
631            } else if (csq instanceof LazyStringBuilder) {
632                sb.append((LazyStringBuilder) csq);
633            } else {
634                sb.append(csq);
635            }
636            break;
637        }
638        return this;
639    }
640
641    @Override
642    public XmlStringBuilder append(CharSequence csq, int start, int end) {
643        assert csq != null;
644        sb.append(csq, start, end);
645        return this;
646    }
647
648    @Override
649    public XmlStringBuilder append(char c) {
650        sb.append(c);
651        return this;
652    }
653
654    @Override
655    public int length() {
656        return sb.length();
657    }
658
659    @Override
660    public char charAt(int index) {
661        return sb.charAt(index);
662    }
663
664    @Override
665    public CharSequence subSequence(int start, int end) {
666        return sb.subSequence(start, end);
667    }
668
669    @Override
670    public String toString() {
671        return sb.toString();
672    }
673
674    @Override
675    public boolean equals(Object other) {
676        if (!(other instanceof CharSequence)) {
677            return false;
678        }
679        CharSequence otherCharSequenceBuilder = (CharSequence) other;
680        return toString().equals(otherCharSequenceBuilder.toString());
681    }
682
683    @Override
684    public int hashCode() {
685        return toString().hashCode();
686    }
687
688    private static final class WrappedIoException extends RuntimeException {
689
690        private static final long serialVersionUID = 1L;
691
692        private final IOException wrappedIoException;
693
694        private WrappedIoException(IOException wrappedIoException) {
695            this.wrappedIoException = wrappedIoException;
696        }
697    }
698
699    /**
700     * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write
701     * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the
702     * XmlStringBuilder contents.
703     *
704     * @param writer TODO javadoc me please
705     * @param enclosingXmlEnvironment the enclosing XML environment.
706     * @throws IOException if an I/O error occurred.
707     */
708    public void write(Writer writer, XmlEnvironment enclosingXmlEnvironment) throws IOException {
709        try {
710            appendXmlTo(csq -> {
711                try {
712                    writer.append(csq);
713                } catch (IOException e) {
714                    throw new WrappedIoException(e);
715                }
716            }, enclosingXmlEnvironment);
717        } catch (WrappedIoException e) {
718            throw e.wrappedIoException;
719        }
720    }
721
722    public List<CharSequence> toList(XmlEnvironment enclosingXmlEnvironment) {
723        List<CharSequence> res = new ArrayList<>(sb.getAsList().size());
724
725        appendXmlTo(csq -> res.add(csq), enclosingXmlEnvironment);
726
727        return res;
728    }
729
730    @Override
731    public StringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) {
732        // This is only the potential length, since the actual length depends on the given XmlEnvironment.
733        int potentialLength = length();
734        StringBuilder res = new StringBuilder(potentialLength);
735
736        appendXmlTo(csq -> res.append(csq), enclosingXmlEnvironment);
737
738        return res;
739    }
740
741    private void appendXmlTo(Consumer<CharSequence> charSequenceSink, XmlEnvironment enclosingXmlEnvironment) {
742        for (CharSequence csq : sb.getAsList()) {
743            if (csq instanceof XmlStringBuilder) {
744                ((XmlStringBuilder) csq).appendXmlTo(charSequenceSink, enclosingXmlEnvironment);
745            }
746            else if (csq instanceof XmlNsAttribute) {
747                XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq;
748                if (!xmlNsAttribute.value.equals(enclosingXmlEnvironment.getEffectiveNamespace())) {
749                    charSequenceSink.accept(xmlNsAttribute);
750                    enclosingXmlEnvironment = new XmlEnvironment(xmlNsAttribute.value);
751                }
752            }
753            else {
754                charSequenceSink.accept(csq);
755            }
756        }
757    }
758}