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