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