1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package io.wcm.caravan.io.jsontransform.source;
21
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.util.Queue;
25 import java.util.Stack;
26
27 import javax.xml.stream.XMLInputFactory;
28 import javax.xml.stream.XMLStreamConstants;
29 import javax.xml.stream.XMLStreamException;
30 import javax.xml.stream.XMLStreamReader;
31
32 import org.apache.commons.lang3.StringUtils;
33 import org.osgi.annotation.versioning.ProviderType;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 import com.google.common.collect.Lists;
38
39 import io.wcm.caravan.io.jsontransform.element.JsonElement;
40
41
42
43
44 @ProviderType
45 public final class XmlSource implements Source {
46
47 private static final Logger LOG = LoggerFactory.getLogger(XmlSource.class);
48
49 private static final char XPATH_SEPARATOR = '/';
50
51 private static final String JSON_KEY_FOR_VALUES_WITHOUT_NAME = "value";
52
53 private static final XMLInputFactory INPUT_FACTORY = XMLInputFactory.newInstance();
54
55 private final XMLStreamReader reader;
56 private final String[] roots;
57
58 private final Queue<JsonElement> outputBuffer = Lists.newLinkedList();
59 private final Stack<String> breadCrumb = new Stack<String>();
60
61 private boolean nextHasBeenExecutedBefore;
62 private boolean uncapitalizeProperties = true;
63
64 private JsonElement firstElement = JsonElement.DEFAULT_START_OBJECT;
65 private JsonElement lastElement = JsonElement.DEFAULT_END_OBJECT;
66
67 static {
68 INPUT_FACTORY.setProperty(XMLInputFactory.IS_COALESCING, true);
69 }
70
71
72
73
74
75
76 public XmlSource(final InputStream input, final String... roots)
77 throws XMLStreamException {
78 reader = INPUT_FACTORY.createXMLStreamReader(input);
79 this.roots = roots;
80 }
81
82 @Override
83 public boolean hasNext() {
84 fillOutputBufferIfNeeded();
85 return !outputBuffer.isEmpty();
86 }
87
88 @Override
89 public JsonElement next() {
90 fillOutputBufferIfNeeded();
91 return outputBuffer.isEmpty() ? null : outputBuffer.poll();
92 }
93
94
95
96
97 public boolean isUncapitalizeProperties() {
98 return this.uncapitalizeProperties;
99 }
100
101
102
103
104 public void setUncapitalizeProperties(boolean uncapitalizeProperties) {
105 this.uncapitalizeProperties = uncapitalizeProperties;
106 }
107
108 private void fillOutputBufferIfNeeded() {
109 if (outputBuffer.isEmpty()) {
110 handleElement();
111 addLastElementToOutputBufferIfNeeded();
112 }
113 }
114
115 private void handleElement() {
116 seekToNextElementIfNeeded();
117 if (reader.isStartElement()) {
118 handleStartElement();
119 }
120 else if (reader.isEndElement()) {
121 addToOutputBuffer(JsonElement.DEFAULT_END_OBJECT);
122 }
123 else if (reader.isCharacters()) {
124 addToOutputBuffer(createValueElement(JSON_KEY_FOR_VALUES_WITHOUT_NAME, reader.getText()));
125 }
126 }
127
128 private void seekToNextElementIfNeeded() {
129 try {
130 if (nextHasBeenExecutedBefore) {
131 nextHasBeenExecutedBefore = false;
132 }
133 else if (reader.hasNext()) {
134 int eventType;
135 do {
136 eventType = reader.next();
137 }
138 while (reader.hasNext() && !isRelevantElement(eventType));
139 }
140 }
141 catch (XMLStreamException ex) {
142 LOG.error("Error reading XML source", ex);
143 }
144 }
145
146 private boolean isRelevantElement(int eventType) {
147 if (reader.isWhiteSpace() || eventType == XMLStreamConstants.COMMENT) {
148 return false;
149 }
150 String xpath = getXPath();
151 if (reader.isStartElement()) {
152 breadCrumb.add(reader.getLocalName());
153 xpath = getXPath();
154 }
155 else if (reader.isEndElement()) {
156 breadCrumb.pop();
157 }
158 return isInOneRoot(xpath);
159 }
160
161 private boolean isInOneRoot(final String xpath) {
162 if (roots.length == 0) {
163 return true;
164 }
165 for (String root : roots) {
166 if (xpath.startsWith(root)) {
167 return true;
168 }
169 }
170 return false;
171 }
172
173 private void handleStartElement() {
174 String name = reader.getLocalName();
175 String key = convertName(name);
176 if (reader.getAttributeCount() > 0) {
177 addToOutputBuffer(JsonElement.startObject(key));
178 addAttributesToOutputBuffer();
179 }
180 else {
181 seekToNextElementIfNeeded();
182 if (reader.isStartElement()) {
183 nextHasBeenExecutedBefore = true;
184 addToOutputBuffer(JsonElement.startObject(key));
185 }
186 else if (reader.isEndElement()) {
187 addToOutputBuffer(JsonElement.nullValue(key));
188 }
189 else {
190 addToOutputBuffer(createValueElement(key, reader.getText()));
191 seekToNextElementIfNeeded();
192 }
193 }
194 }
195
196 private void addToOutputBuffer(JsonElement element) {
197 addFirstElementToOutputBufferIfNeeded();
198 outputBuffer.add(element);
199 }
200
201 private void addFirstElementToOutputBufferIfNeeded() {
202 if (firstElement != null) {
203 outputBuffer.add(firstElement);
204 firstElement = null;
205 }
206 }
207
208 private void addAttributesToOutputBuffer() {
209 for (int i = 0; i < reader.getAttributeCount(); i++) {
210 addToOutputBuffer(JsonElement.value(convertName(reader.getAttributeLocalName(i)), reader.getAttributeValue(i)));
211 }
212 }
213
214 private String convertName(final String name) {
215 if (uncapitalizeProperties) {
216 return StringUtils.uncapitalize(name);
217 }
218 return name;
219 }
220
221 private JsonElement createValueElement(final String key, final String value) {
222 return JsonElement.value(key, value);
223 }
224
225 private void addLastElementToOutputBufferIfNeeded() {
226 if (outputBuffer.isEmpty() && firstElement == null && lastElement != null) {
227 outputBuffer.add(lastElement);
228 lastElement = null;
229 }
230 }
231
232 private String getXPath() {
233 return StringUtils.join(breadCrumb, XPATH_SEPARATOR);
234 }
235
236 @Override
237 public void close() throws IOException {
238 try {
239 reader.close();
240 }
241 catch (XMLStreamException ex) {
242 throw new IOException(ex);
243 }
244 }
245
246
247 }