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