View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
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   * Parses the SOAP response and transforms into JSON elements.
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     * @param input The input stream
73     * @param roots Possible XML roots
74     * @throws XMLStreamException XML read error
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     * @return true if all JSON property names should be converted to start with a lower-case letter
96     */
97    public boolean isUncapitalizeProperties() {
98      return this.uncapitalizeProperties;
99    }
100 
101   /**
102    * @param uncapitalizeProperties true if all JSON property names should be converted to start with a lower-case letter
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 }