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