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