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