001/**
002 *
003 * Copyright 2014-2021 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.ExtensionElement;
028import org.jivesoftware.smack.packet.FullyQualifiedElement;
029import org.jivesoftware.smack.packet.NamedElement;
030import org.jivesoftware.smack.packet.XmlEnvironment;
031
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(ExtensionElement pe) {
047        this(pe, null);
048    }
049
050    public XmlStringBuilder(NamedElement e) {
051        this();
052        halfOpenElement(e.getElementName());
053    }
054
055    public XmlStringBuilder(FullyQualifiedElement 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        // TODO: Should use toString() instead of name().
293        attribute(name, value.name());
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(FullyQualifiedElement 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        sb.append(csq);
602        return this;
603    }
604
605    @Override
606    public XmlStringBuilder append(CharSequence csq, int start, int end) {
607        assert csq != null;
608        sb.append(csq, start, end);
609        return this;
610    }
611
612    @Override
613    public XmlStringBuilder append(char c) {
614        sb.append(c);
615        return this;
616    }
617
618    @Override
619    public int length() {
620        return sb.length();
621    }
622
623    @Override
624    public char charAt(int index) {
625        return sb.charAt(index);
626    }
627
628    @Override
629    public CharSequence subSequence(int start, int end) {
630        return sb.subSequence(start, end);
631    }
632
633    @Override
634    public String toString() {
635        return sb.toString();
636    }
637
638    @Override
639    public boolean equals(Object other) {
640        if (!(other instanceof CharSequence)) {
641            return false;
642        }
643        CharSequence otherCharSequenceBuilder = (CharSequence) other;
644        return toString().equals(otherCharSequenceBuilder.toString());
645    }
646
647    @Override
648    public int hashCode() {
649        return toString().hashCode();
650    }
651
652    private static final class WrappedIoException extends RuntimeException {
653
654        private static final long serialVersionUID = 1L;
655
656        private final IOException wrappedIoException;
657
658        private WrappedIoException(IOException wrappedIoException) {
659            this.wrappedIoException = wrappedIoException;
660        }
661    }
662
663    /**
664     * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write
665     * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the
666     * XmlStringBuilder contents.
667     *
668     * @param writer TODO javadoc me please
669     * @param enclosingXmlEnvironment the enclosing XML environment.
670     * @throws IOException if an I/O error occurred.
671     */
672    public void write(Writer writer, XmlEnvironment enclosingXmlEnvironment) throws IOException {
673        try {
674            appendXmlTo(csq -> {
675                try {
676                    writer.append(csq);
677                } catch (IOException e) {
678                    throw new WrappedIoException(e);
679                }
680            }, enclosingXmlEnvironment);
681        } catch (WrappedIoException e) {
682            throw e.wrappedIoException;
683        }
684    }
685
686    public List<CharSequence> toList(XmlEnvironment enclosingXmlEnvironment) {
687        List<CharSequence> res = new ArrayList<>(sb.getAsList().size());
688
689        appendXmlTo(csq -> res.add(csq), enclosingXmlEnvironment);
690
691        return res;
692    }
693
694    @Override
695    public StringBuilder toXML(XmlEnvironment enclosingXmlEnvironment) {
696        // This is only the potential length, since the actual length depends on the given XmlEnvironment.
697        int potentialLength = length();
698        StringBuilder res = new StringBuilder(potentialLength);
699
700        appendXmlTo(csq -> res.append(csq), enclosingXmlEnvironment);
701
702        return res;
703    }
704
705    private void appendXmlTo(Consumer<CharSequence> charSequenceSink, XmlEnvironment enclosingXmlEnvironment) {
706        for (CharSequence csq : sb.getAsList()) {
707            if (csq instanceof XmlStringBuilder) {
708                ((XmlStringBuilder) csq).appendXmlTo(charSequenceSink, enclosingXmlEnvironment);
709            }
710            else if (csq instanceof XmlNsAttribute) {
711                XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq;
712                if (!xmlNsAttribute.value.equals(enclosingXmlEnvironment.getEffectiveNamespace())) {
713                    charSequenceSink.accept(xmlNsAttribute);
714                    enclosingXmlEnvironment = new XmlEnvironment(xmlNsAttribute.value);
715                }
716            }
717            else {
718                charSequenceSink.accept(csq);
719            }
720        }
721    }
722}