XmlSource.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.caravan.io.jsontransform.source;
import java.io.IOException;
import java.io.InputStream;
import java.util.Queue;
import java.util.Stack;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.apache.commons.lang3.StringUtils;
import org.osgi.annotation.versioning.ProviderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import io.wcm.caravan.io.jsontransform.element.JsonElement;
/**
* Parses the SOAP response and transforms into JSON elements.
*/
@ProviderType
public final class XmlSource implements Source {
private static final Logger LOG = LoggerFactory.getLogger(XmlSource.class);
private static final char XPATH_SEPARATOR = '/';
private static final String JSON_KEY_FOR_VALUES_WITHOUT_NAME = "value";
private static final XMLInputFactory INPUT_FACTORY = XMLInputFactory.newInstance();
private final XMLStreamReader reader;
private final String[] roots;
private final Queue<JsonElement> outputBuffer = Lists.newLinkedList();
private final Stack<String> breadCrumb = new Stack<String>();
private boolean nextHasBeenExecutedBefore;
private boolean uncapitalizeProperties = true;
private JsonElement firstElement = JsonElement.DEFAULT_START_OBJECT;
private JsonElement lastElement = JsonElement.DEFAULT_END_OBJECT;
static {
INPUT_FACTORY.setProperty(XMLInputFactory.IS_COALESCING, true);
}
/**
* @param input The input stream
* @param roots Possible XML roots
* @throws XMLStreamException XML read error
*/
public XmlSource(final InputStream input, final String... roots)
throws XMLStreamException {
reader = INPUT_FACTORY.createXMLStreamReader(input);
this.roots = roots;
}
@Override
public boolean hasNext() {
fillOutputBufferIfNeeded();
return !outputBuffer.isEmpty();
}
@Override
public JsonElement next() {
fillOutputBufferIfNeeded();
return outputBuffer.isEmpty() ? null : outputBuffer.poll();
}
/**
* @return true if all JSON property names should be converted to start with a lower-case letter
*/
public boolean isUncapitalizeProperties() {
return this.uncapitalizeProperties;
}
/**
* @param uncapitalizeProperties true if all JSON property names should be converted to start with a lower-case letter
*/
public void setUncapitalizeProperties(boolean uncapitalizeProperties) {
this.uncapitalizeProperties = uncapitalizeProperties;
}
private void fillOutputBufferIfNeeded() {
if (outputBuffer.isEmpty()) {
handleElement();
addLastElementToOutputBufferIfNeeded();
}
}
private void handleElement() {
seekToNextElementIfNeeded();
if (reader.isStartElement()) {
handleStartElement();
}
else if (reader.isEndElement()) {
addToOutputBuffer(JsonElement.DEFAULT_END_OBJECT);
}
else if (reader.isCharacters()) {
addToOutputBuffer(createValueElement(JSON_KEY_FOR_VALUES_WITHOUT_NAME, reader.getText()));
}
}
private void seekToNextElementIfNeeded() {
try {
if (nextHasBeenExecutedBefore) {
nextHasBeenExecutedBefore = false;
}
else if (reader.hasNext()) {
int eventType;
do {
eventType = reader.next();
}
while (reader.hasNext() && !isRelevantElement(eventType));
}
}
catch (XMLStreamException ex) {
LOG.error("Error reading XML source", ex);
}
}
private boolean isRelevantElement(int eventType) {
if (reader.isWhiteSpace() || eventType == XMLStreamConstants.COMMENT) {
return false;
}
String xpath = getXPath();
if (reader.isStartElement()) {
breadCrumb.add(reader.getLocalName());
xpath = getXPath();
}
else if (reader.isEndElement()) {
breadCrumb.pop();
}
return isInOneRoot(xpath);
}
private boolean isInOneRoot(final String xpath) {
if (roots.length == 0) {
return true;
}
for (String root : roots) {
if (xpath.startsWith(root)) {
return true;
}
}
return false;
}
private void handleStartElement() {
String name = reader.getLocalName();
String key = convertName(name);
if (reader.getAttributeCount() > 0) {
addToOutputBuffer(JsonElement.startObject(key));
addAttributesToOutputBuffer();
}
else {
seekToNextElementIfNeeded();
if (reader.isStartElement()) {
nextHasBeenExecutedBefore = true;
addToOutputBuffer(JsonElement.startObject(key));
}
else if (reader.isEndElement()) {
addToOutputBuffer(JsonElement.nullValue(key));
}
else {
addToOutputBuffer(createValueElement(key, reader.getText()));
seekToNextElementIfNeeded();
}
}
}
private void addToOutputBuffer(JsonElement element) {
addFirstElementToOutputBufferIfNeeded();
outputBuffer.add(element);
}
private void addFirstElementToOutputBufferIfNeeded() {
if (firstElement != null) {
outputBuffer.add(firstElement);
firstElement = null;
}
}
private void addAttributesToOutputBuffer() {
for (int i = 0; i < reader.getAttributeCount(); i++) {
addToOutputBuffer(JsonElement.value(convertName(reader.getAttributeLocalName(i)), reader.getAttributeValue(i)));
}
}
private String convertName(final String name) {
if (uncapitalizeProperties) {
return StringUtils.uncapitalize(name);
}
return name;
}
private JsonElement createValueElement(final String key, final String value) {
return JsonElement.value(key, value);
}
private void addLastElementToOutputBufferIfNeeded() {
if (outputBuffer.isEmpty() && firstElement == null && lastElement != null) {
outputBuffer.add(lastElement);
lastElement = null;
}
}
private String getXPath() {
return StringUtils.join(breadCrumb, XPATH_SEPARATOR);
}
@Override
public void close() throws IOException {
try {
reader.close();
}
catch (XMLStreamException ex) {
throw new IOException(ex);
}
}
}