001/**
002 *
003 * Copyright 2014-2018 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.Collection;
022import java.util.Date;
023
024import org.jivesoftware.smack.packet.Element;
025import org.jivesoftware.smack.packet.ExtensionElement;
026import org.jivesoftware.smack.packet.NamedElement;
027
028import org.jxmpp.util.XmppDateTime;
029
030public class XmlStringBuilder implements Appendable, CharSequence, Element {
031    public static final String RIGHT_ANGLE_BRACKET = Character.toString('>');
032
033    private final LazyStringBuilder sb;
034
035    private final String enclosingNamespace;
036
037    public XmlStringBuilder() {
038        this("");
039    }
040
041    public XmlStringBuilder(String enclosingNamespace) {
042        sb = new LazyStringBuilder();
043        this.enclosingNamespace = enclosingNamespace != null ? enclosingNamespace : "";
044    }
045
046    public XmlStringBuilder(ExtensionElement pe) {
047        this();
048        prelude(pe);
049    }
050
051    public XmlStringBuilder(NamedElement e) {
052        this();
053        halfOpenElement(e.getElementName());
054    }
055
056    public XmlStringBuilder(ExtensionElement ee, String enclosingNamespace) {
057        this(enclosingNamespace);
058        String namespace = ee.getNamespace();
059        prelude(ee);
060    }
061
062    public XmlStringBuilder escapedElement(String name, String escapedContent) {
063        assert escapedContent != null;
064        openElement(name);
065        append(escapedContent);
066        closeElement(name);
067        return this;
068    }
069
070    /**
071     * Add a new element to this builder.
072     *
073     * @param name
074     * @param content
075     * @return the XmlStringBuilder
076     */
077    public XmlStringBuilder element(String name, String content) {
078        if (content.isEmpty()) {
079            return emptyElement(name);
080        }
081        openElement(name);
082        escape(content);
083        closeElement(name);
084        return this;
085    }
086
087    /**
088     * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
089     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
090     *
091     * @param name element name
092     * @param content content of element
093     * @return this XmlStringBuilder
094     */
095    public XmlStringBuilder element(String name, Date content) {
096        assert content != null;
097        return element(name, XmppDateTime.formatXEP0082Date(content));
098    }
099
100   /**
101    * Add a new element to this builder.
102    *
103    * @param name
104    * @param content
105    * @return the XmlStringBuilder
106    */
107   public XmlStringBuilder element(String name, CharSequence content) {
108       return element(name, content.toString());
109   }
110
111    public XmlStringBuilder element(String name, Enum<?> content) {
112        assert content != null;
113        element(name, content.name());
114        return this;
115    }
116
117    public XmlStringBuilder element(Element element) {
118        assert element != null;
119        return append(element.toXML(null));
120    }
121
122    public XmlStringBuilder optElement(String name, String content) {
123        if (content != null) {
124            element(name, content);
125        }
126        return this;
127    }
128
129    /**
130     * Add a new element to this builder, with the {@link java.util.Date} instance as its content,
131     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
132     * if {@link java.util.Date} instance is not <code>null</code>.
133     *
134     * @param name element name
135     * @param content content of element
136     * @return this XmlStringBuilder
137     */
138    public XmlStringBuilder optElement(String name, Date content) {
139        if (content != null) {
140            element(name, content);
141        }
142        return this;
143    }
144
145    public XmlStringBuilder optElement(String name, CharSequence content) {
146        if (content != null) {
147            element(name, content.toString());
148        }
149        return this;
150    }
151
152    public XmlStringBuilder optElement(Element element) {
153        if (element != null) {
154            append(element.toXML(null));
155        }
156        return this;
157    }
158
159    public XmlStringBuilder optElement(String name, Enum<?> content) {
160        if (content != null) {
161            element(name, content);
162        }
163        return this;
164    }
165
166    public XmlStringBuilder optElement(String name, Object object) {
167        if (object != null) {
168            element(name, object.toString());
169        }
170        return this;
171    }
172
173    public XmlStringBuilder optIntElement(String name, int value) {
174        if (value >= 0) {
175            element(name, String.valueOf(value));
176        }
177        return this;
178    }
179
180    public XmlStringBuilder halfOpenElement(String name) {
181        assert (StringUtils.isNotEmpty(name));
182        sb.append('<').append(name);
183        return this;
184    }
185
186    public XmlStringBuilder halfOpenElement(NamedElement namedElement) {
187        return halfOpenElement(namedElement.getElementName());
188    }
189
190    public XmlStringBuilder openElement(String name) {
191        halfOpenElement(name).rightAngleBracket();
192        return this;
193    }
194
195    public XmlStringBuilder closeElement(String name) {
196        sb.append("</").append(name);
197        rightAngleBracket();
198        return this;
199    }
200
201    public XmlStringBuilder closeElement(NamedElement e) {
202        closeElement(e.getElementName());
203        return this;
204    }
205
206    public XmlStringBuilder closeEmptyElement() {
207        sb.append("/>");
208        return this;
209    }
210
211    /**
212     * Add a right angle bracket '&gt;'.
213     *
214     * @return a reference to this object.
215     */
216    public XmlStringBuilder rightAngleBracket() {
217        sb.append(RIGHT_ANGLE_BRACKET);
218        return this;
219    }
220
221    /**
222     * Add a right angle bracket '&gt;'.
223     *
224     * @return a reference to this object
225     * @deprecated use {@link #rightAngleBracket()} instead
226     */
227    @Deprecated
228    public XmlStringBuilder rightAngelBracket() {
229        return rightAngleBracket();
230    }
231
232    /**
233     * Does nothing if value is null.
234     *
235     * @param name
236     * @param value
237     * @return the XmlStringBuilder
238     */
239    public XmlStringBuilder attribute(String name, String value) {
240        assert value != null;
241        sb.append(' ').append(name).append("='");
242        escapeAttributeValue(value);
243        sb.append('\'');
244        return this;
245    }
246
247    public XmlStringBuilder attribute(String name, boolean bool) {
248        return attribute(name, Boolean.toString(bool));
249    }
250
251    /**
252     * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
253     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}.
254     *
255     * @param name name of attribute
256     * @param value value of attribute
257     * @return this XmlStringBuilder
258     */
259    public XmlStringBuilder attribute(String name, Date value) {
260        assert value != null;
261        return attribute(name, XmppDateTime.formatXEP0082Date(value));
262    }
263
264    public XmlStringBuilder attribute(String name, CharSequence value) {
265        return attribute(name, value.toString());
266    }
267
268    public XmlStringBuilder attribute(String name, Enum<?> value) {
269        assert value != null;
270        attribute(name, value.name());
271        return this;
272    }
273
274    public XmlStringBuilder attribute(String name, int value) {
275        assert name != null;
276        return attribute(name, String.valueOf(value));
277    }
278
279    public XmlStringBuilder optAttribute(String name, String value) {
280        if (value != null) {
281            attribute(name, value);
282        }
283        return this;
284    }
285
286    /**
287     * Add a new attribute to this builder, with the {@link java.util.Date} instance as its value,
288     * which will get formatted with {@link XmppDateTime#formatXEP0082Date(Date)}
289     * if {@link java.util.Date} instance is not <code>null</code>.
290     *
291     * @param name attribute name
292     * @param value value of this attribute
293     * @return this XmlStringBuilder
294     */
295    public XmlStringBuilder optAttribute(String name, Date value) {
296        if (value != null) {
297            attribute(name, value);
298        }
299        return this;
300    }
301
302    public XmlStringBuilder optAttribute(String name, CharSequence value) {
303        if (value != null) {
304            attribute(name, value.toString());
305        }
306        return this;
307    }
308
309    public XmlStringBuilder optAttribute(String name, Enum<?> value) {
310        if (value != null) {
311            attribute(name, value.toString());
312        }
313        return this;
314    }
315
316    /**
317     * Add the given attribute if {@code value => 0}.
318     *
319     * @param name
320     * @param value
321     * @return a reference to this object
322     */
323    public XmlStringBuilder optIntAttribute(String name, int value) {
324        if (value >= 0) {
325            attribute(name, Integer.toString(value));
326        }
327        return this;
328    }
329
330    /**
331     * Add the given attribute if value not null and {@code value => 0}.
332     *
333     * @param name
334     * @param value
335     * @return a reference to this object
336     */
337    public XmlStringBuilder optLongAttribute(String name, Long value) {
338        if (value != null && value >= 0) {
339            attribute(name, Long.toString(value));
340        }
341        return this;
342    }
343
344    public XmlStringBuilder optBooleanAttribute(String name, boolean bool) {
345        if (bool) {
346            sb.append(' ').append(name).append("='true'");
347        }
348        return this;
349    }
350
351    public XmlStringBuilder optBooleanAttributeDefaultTrue(String name, boolean bool) {
352        if (!bool) {
353            sb.append(' ').append(name).append("='false'");
354        }
355        return this;
356    }
357
358    private static final class XmlNsAttribute implements CharSequence {
359        private final String value;
360        private final String xmlFragment;
361
362        private XmlNsAttribute(String value) {
363            this.value = StringUtils.requireNotNullOrEmpty(value, "Value must not be null");
364            this.xmlFragment = " xmlns='" + value + '\'';
365        }
366
367        @Override
368        public String toString() {
369            return xmlFragment;
370        }
371
372        @Override
373        public int length() {
374            return xmlFragment.length();
375        }
376
377        @Override
378        public char charAt(int index) {
379            return xmlFragment.charAt(index);
380        }
381
382        @Override
383        public CharSequence subSequence(int start, int end) {
384            return xmlFragment.subSequence(start, end);
385        }
386    }
387
388    public XmlStringBuilder xmlnsAttribute(String value) {
389        if (value != null && !enclosingNamespace.equals(value)) {
390            XmlNsAttribute xmlNsAttribute = new XmlNsAttribute(value);
391            append(xmlNsAttribute);
392        }
393        return this;
394    }
395
396    public XmlStringBuilder xmllangAttribute(String value) {
397        optAttribute("xml:lang", value);
398        return this;
399    }
400
401    public XmlStringBuilder optXmlLangAttribute(String lang) {
402        if (!StringUtils.isNullOrEmpty(lang)) {
403            xmllangAttribute(lang);
404        }
405        return this;
406    }
407
408    public XmlStringBuilder escape(String text) {
409        assert text != null;
410        sb.append(StringUtils.escapeForXml(text));
411        return this;
412    }
413
414    public XmlStringBuilder escapeAttributeValue(String value) {
415        assert value != null;
416        sb.append(StringUtils.escapeForXmlAttributeApos(value));
417        return this;
418    }
419
420    public XmlStringBuilder optEscape(CharSequence text) {
421        if (text == null) {
422            return this;
423        }
424        return escape(text);
425    }
426
427    public XmlStringBuilder escape(CharSequence text) {
428        return escape(text.toString());
429    }
430
431    public XmlStringBuilder prelude(ExtensionElement pe) {
432        return prelude(pe.getElementName(), pe.getNamespace());
433    }
434
435    public XmlStringBuilder prelude(String elementName, String namespace) {
436        halfOpenElement(elementName);
437        xmlnsAttribute(namespace);
438        return this;
439    }
440
441    public XmlStringBuilder optAppend(CharSequence csq) {
442        if (csq != null) {
443            append(csq);
444        }
445        return this;
446    }
447
448    public XmlStringBuilder optAppend(Element element) {
449        if (element != null) {
450            append(element.toXML(enclosingNamespace));
451        }
452        return this;
453    }
454
455    public XmlStringBuilder append(XmlStringBuilder xsb) {
456        assert xsb != null;
457        sb.append(xsb.sb);
458        return this;
459    }
460
461    public XmlStringBuilder append(Collection<? extends Element> elements) {
462        return append(elements, null);
463    }
464
465    public XmlStringBuilder append(Collection<? extends Element> elements, String enclosingNamespace) {
466        for (Element element : elements) {
467            append(element.toXML(enclosingNamespace));
468        }
469        return this;
470    }
471
472    public XmlStringBuilder emptyElement(Enum<?> element) {
473        return emptyElement(element.name());
474    }
475
476    public XmlStringBuilder emptyElement(String element) {
477        halfOpenElement(element);
478        return closeEmptyElement();
479    }
480
481    public XmlStringBuilder condEmptyElement(boolean condition, String element) {
482        if (condition) {
483            emptyElement(element);
484        }
485        return this;
486    }
487
488    public XmlStringBuilder condAttribute(boolean condition, String name, String value) {
489        if (condition) {
490            attribute(name, value);
491        }
492        return this;
493    }
494
495    @Override
496    public XmlStringBuilder append(CharSequence csq) {
497        assert csq != null;
498        sb.append(csq);
499        return this;
500    }
501
502    @Override
503    public XmlStringBuilder append(CharSequence csq, int start, int end) {
504        assert csq != null;
505        sb.append(csq, start, end);
506        return this;
507    }
508
509    @Override
510    public XmlStringBuilder append(char c) {
511        sb.append(c);
512        return this;
513    }
514
515    @Override
516    public int length() {
517        return sb.length();
518    }
519
520    @Override
521    public char charAt(int index) {
522        return sb.charAt(index);
523    }
524
525    @Override
526    public CharSequence subSequence(int start, int end) {
527        return sb.subSequence(start, end);
528    }
529
530    @Override
531    public String toString() {
532        return sb.toString();
533    }
534
535    @Override
536    public boolean equals(Object other) {
537        if (!(other instanceof CharSequence)) {
538            return false;
539        }
540        CharSequence otherCharSequenceBuilder = (CharSequence) other;
541        return toString().equals(otherCharSequenceBuilder.toString());
542    }
543
544    @Override
545    public int hashCode() {
546        return toString().hashCode();
547    }
548
549    /**
550     * Write the contents of this <code>XmlStringBuilder</code> to a {@link Writer}. This will write
551     * the single parts one-by-one, avoiding allocation of a big continuous memory block holding the
552     * XmlStringBuilder contents.
553     *
554     * @param writer
555     * @throws IOException
556     */
557    public void write(Writer writer, String enclosingNamespace) throws IOException {
558        for (CharSequence csq : sb.getAsList()) {
559            if (csq instanceof XmlStringBuilder) {
560                ((XmlStringBuilder) csq).write(writer, enclosingNamespace);
561            }
562            else if (csq instanceof XmlNsAttribute) {
563                XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq;
564                if (!xmlNsAttribute.value.equals(enclosingNamespace)) {
565                    writer.write(xmlNsAttribute.toString());
566                    enclosingNamespace = xmlNsAttribute.value;
567                }
568            }
569            else {
570                writer.write(csq.toString());
571            }
572        }
573    }
574
575    @Override
576    public CharSequence toXML(String enclosingNamespace) {
577        StringBuilder res = new StringBuilder();
578        appendXmlTo(res, enclosingNamespace);
579        return res;
580    }
581
582    private void appendXmlTo(StringBuilder res, String enclosingNamespace) {
583        for (CharSequence csq : sb.getAsList()) {
584            if (csq instanceof XmlStringBuilder) {
585                ((XmlStringBuilder) csq).appendXmlTo(res, enclosingNamespace);
586            }
587            else if (csq instanceof XmlNsAttribute) {
588                XmlNsAttribute xmlNsAttribute = (XmlNsAttribute) csq;
589                if (!xmlNsAttribute.value.equals(enclosingNamespace)) {
590                    sb.append(xmlNsAttribute);
591                    enclosingNamespace = xmlNsAttribute.value;
592                }
593            }
594            else {
595                res.append(csq);
596            }
597        }
598    }
599}