aboutsummaryrefslogtreecommitdiffstats
path: root/document/src/main/java/com/yahoo/document/serialization/XmlStream.java
blob: 2c3e2eeb1ac063d42c9bfe094caac840eec947a7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.document.serialization;

import com.yahoo.text.XML;

import java.io.StringWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.ListIterator;

/**
 * Class for writing XML in a simplified way.
 * <p>
 * Give a writer for it to write the XML directly to. If none is given a
 * StringWriter is used so you can call toString() on the class to get the
 * XML.
 * <p>
 * You build XML by calling beginTag(name), addAttribute(id, value), endTag().
 * Remember to close all your tags, or you'll get an exception when calling
 * toString(). If writing directly to a writer, call isFinalized to verify that
 * all tags have been closed.
 * <p>
 * The XML escaping tools only give an interface for escape from and to a string
 * value. Thus writing of all data here is also just available through strings.
 *
 * @author <a href="humbe@yahoo-inc.com">Haakon Humberset</a>
 */
@Deprecated
public class XmlStream {

    // Utility class to hold attributes internally until it's time to write them
    private static class Attribute {
        final String name;
        final String value;

        public Attribute(String name, Object value) {
            this.name = name;
            this.value = value.toString();
        }
    }

    private final StringWriter writer; // Writer to output XML to.
    private final Deque<String> tags = new ArrayDeque<String>();
    private String indent = "";
    // We write tags lazily for several reasons:
    //   - To allow recursive methods to have both parents and child add
    //     attributes to last tag, without giving child responsibility of
    //     closing or creating the tag.
    //   - Be able to check content before adding whitespace, such that we
    //     can add newlines if content is new tags for instance.
    // The cached variables here will be written with the flush() method.
    private String cachedTag = null;
    private final List<Attribute> cachedAttribute = new ArrayList<Attribute>();
    private final List<String> cachedContent = new ArrayList<String>();

    /**
     * Create an XmlStream writing to a StringWriter.
     * Fetch XML through toString() once you're done creating it.
     */
    public XmlStream() {
        writer = new StringWriter();
    }

    /**
     * Set an indent to use for pretty printing of XML. Default is no indent.
     *
     * @param indent the initial indentation
     */
    public void setIndent(String indent) {
        this.indent = indent;
    }

    /**
     * Check if all tags have been properly closed.
     *
     * @return true if all tags are closed
     */
    public boolean isFinalized() {
        return (tags.isEmpty() && cachedTag == null);
    }

    public String toString() {
        if (!isFinalized()) {
            throw new IllegalStateException("There are still" + " tag(s) that are not closed.");
        }
        StringWriter sw = writer; // Ensure we have string writer
        return sw.toString();
    }

    /**
     * Add a new XML tag with the given name.
     *
     * @param name the tag name
     */
    public void beginTag(String name) {
        if (!XML.isName(name)) {
            throw new IllegalArgumentException("The name '" + name
                    + "' cannot be used as an XML tag name. Legal names must adhere to"
                    + "http://www.w3.org/TR/2006/REC-xml11-20060816/#sec-common-syn");
        }
        if (cachedTag != null) flush(false);
        cachedTag = name;
    }

    /**
     * Add a new XML attribute to the last tag started.
     * The tag cannot already have had content added to it, or been ended.
     * If a null value is added, the attribute will be skipped.
     *
     * @param key   the attribute name
     * @param value the attribute value
     */
    public void addAttribute(String key, Object value) {
        if (value == null) {
            return;
        }
        if (cachedTag == null) {
            throw new IllegalStateException("There is no open tag to add attributes to.");
        }
        if (!XML.isName(key)) {
            throw new IllegalArgumentException("The name '" + key
                    + "' cannot be used as an XML attribute name. Legal names must adhere to"
                    + " http://www.w3.org/TR/2006/REC-xml11-20060816/#sec-common-syn");
        }
        cachedAttribute.add(new Attribute(key, value));
    }

    /**
     * Add content to the last tag.
     *
     * @param content the content to add to the last tag
     */
    public void addContent(String content) {
        if (cachedTag != null) {
            cachedContent.add(XML.xmlEscape(content, false));
        } else if (tags.isEmpty()) {
            throw new IllegalStateException("There is no open tag to add content to.");
        } else {
            for (int i = 0; i < tags.size(); ++i) {
                writer.write(indent);
            }
            writer.write(XML.xmlEscape(content, false));
            writer.write('\n');
        }
    }

    /**
     * Ends the last tag created.
     *
     */
    public void endTag() {
        if (cachedTag != null) {
            flush(true);
        } else if (tags.isEmpty()) {
            throw new IllegalStateException("Cannot end non-existing tag");
        } else {
            for (int i = 1; i < tags.size(); ++i) {
                writer.write(indent);
            }
            writer.write("</");
            writer.write(tags.removeFirst());
            writer.write(">\n");
        }
    }

    // Utility function to write whatever is cached.
    private void flush(boolean endTag) {
        if (cachedTag == null) {
            throw new IllegalStateException("Cannot write non-existing tag");
        }
        for (int i = 0; i < tags.size(); ++i) {
            writer.write(indent);
        }
        writer.write('<');
        writer.write(cachedTag);
        for (ListIterator<Attribute> it = cachedAttribute.listIterator(); it.hasNext();) {
            Attribute attr = it.next();
            writer.write(' ');
            writer.write(attr.name);
            writer.write("=\"");
            writer.write(XML.xmlEscape(attr.value, true));
            writer.write('"');
        }
        cachedAttribute.clear();
        if (cachedContent.isEmpty() && endTag) {
            writer.write("/>\n");
        } else if (cachedContent.isEmpty()) {
            writer.write(">\n");
            tags.addFirst(cachedTag);
        } else {
            writer.write(">");
            if (!endTag) {
                writer.write('\n');
                for (int i = 0; i <= tags.size(); ++i) {
                    writer.write(indent);
                }
            }
            for (String content : cachedContent) {
                writer.write(content);
            }
            cachedContent.clear();
            if (endTag) {
                writer.write("</");
                writer.write(cachedTag);
                writer.write(">\n");
            } else {
                writer.write('\n');
                tags.addFirst(cachedTag);
            }
        }
        cachedTag = null;
    }

}