| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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. |
| */ |
| |
| package org.apache.camel.component.xj; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| |
| import javax.xml.namespace.NamespaceContext; |
| import javax.xml.stream.XMLStreamConstants; |
| import javax.xml.stream.XMLStreamException; |
| import javax.xml.stream.XMLStreamWriter; |
| |
| import com.fasterxml.jackson.core.JsonGenerator; |
| import com.fasterxml.jackson.core.JsonToken; |
| |
| /** |
| * XML Json bridge. Explicitly using StreamWriter and not XMLEventWriter because saxon wants that. |
| */ |
| public class XmlJsonStreamWriter implements XMLStreamWriter { |
| |
| private final JsonGenerator jsonGenerator; |
| |
| /** |
| * Root element of the internal tree |
| */ |
| private TreeElement treeRoot; |
| |
| /** |
| * Reference to current "level-s" element |
| */ |
| private TreeElement currentTreeElement; |
| |
| /** |
| * Creates a new XmlJsonStreamWriter instance |
| * @param jsonGenerator the {@link JsonGenerator} to use to write the json document |
| */ |
| public XmlJsonStreamWriter(JsonGenerator jsonGenerator) { |
| this.jsonGenerator = jsonGenerator; |
| } |
| |
| @Override |
| public void writeStartElement(String localName) { |
| writeStartElement(null, localName, null); |
| } |
| |
| @Override |
| public void writeStartElement(String namespaceURI, String localName) { |
| writeStartElement(null, localName, namespaceURI); |
| } |
| |
| @Override |
| public void writeStartElement(String prefix, String localName, String namespaceURI) { |
| final TreeElement treeElement = new TreeElement(currentTreeElement, XMLStreamConstants.START_ELEMENT, localName); |
| |
| currentTreeElement.addChild(treeElement); |
| currentTreeElement = treeElement; |
| } |
| |
| @Override |
| public void writeEmptyElement(String namespaceURI, String localName) { |
| writeStartElement(null, namespaceURI, localName); |
| writeEndElement(); |
| } |
| |
| @Override |
| public void writeEmptyElement(String prefix, String localName, String namespaceURI) { |
| writeStartElement(prefix, localName, namespaceURI); |
| writeEndElement(); |
| } |
| |
| @Override |
| public void writeEmptyElement(String localName) { |
| writeStartElement(null, localName, null); |
| writeEndElement(); |
| } |
| |
| @Override |
| public void writeEndElement() { |
| currentTreeElement.writeEnd(); |
| currentTreeElement = currentTreeElement.parent; |
| } |
| |
| @Override |
| public void writeEndDocument() throws XMLStreamException { |
| try { |
| treeRoot.write(jsonGenerator); |
| } catch (IOException e) { |
| throw new XMLStreamException(e); |
| } |
| } |
| |
| @Override |
| public void close() throws XMLStreamException { |
| try { |
| jsonGenerator.close(); |
| } catch (IOException e) { |
| throw new XMLStreamException(e); |
| } |
| } |
| |
| @Override |
| public void flush() throws XMLStreamException { |
| try { |
| jsonGenerator.flush(); |
| } catch (IOException e) { |
| throw new XMLStreamException(e); |
| } |
| } |
| |
| @Override |
| public void writeAttribute(String localName, String value) { |
| writeAttribute(null, null, localName, value); |
| } |
| |
| @Override |
| public void writeAttribute(String prefix, String namespaceURI, String localName, String value) { |
| if (XJConstants.NS_XJ.equals(namespaceURI)) { |
| switch (localName) { |
| case XJConstants.TYPE_HINT_NAME: |
| currentTreeElement.setName(value); |
| return; |
| case XJConstants.TYPE_HINT_TYPE: |
| currentTreeElement.setJsonToken(XJConstants.TYPE_JSONTYPE_MAP.get(value)); |
| return; |
| default: |
| return; |
| } |
| } |
| |
| final TreeElement treeElement = new TreeElement(currentTreeElement, XMLStreamConstants.ATTRIBUTE, JsonToken.VALUE_STRING, localName); |
| treeElement.setValue(value); |
| |
| currentTreeElement.addChild(treeElement); |
| } |
| |
| @Override |
| public void writeAttribute(String namespaceURI, String localName, String value) { |
| writeAttribute(null, namespaceURI, localName, value); |
| } |
| |
| @Override |
| public void writeNamespace(String prefix, String namespaceURI) { |
| // ignore event - we don't care |
| } |
| |
| @Override |
| public void writeDefaultNamespace(String namespaceURI) { |
| // ignore event - we don't care |
| } |
| |
| @Override |
| public void writeComment(String data) { |
| // ignore event - we don't care |
| } |
| |
| @Override |
| public void writeProcessingInstruction(String target) { |
| // ignore event - we don't care |
| } |
| |
| @Override |
| public void writeProcessingInstruction(String target, String data) { |
| // ignore event - we don't care |
| } |
| |
| @Override |
| public void writeCData(String data) { |
| writeCharacters(data); |
| } |
| |
| @Override |
| public void writeDTD(String dtd) { |
| // ignore event - we don't care |
| } |
| |
| @Override |
| public void writeEntityRef(String name) { |
| // ignore event - we don't care |
| } |
| |
| @Override |
| public void writeStartDocument() { |
| writeStartDocument(null); |
| } |
| |
| @Override |
| public void writeStartDocument(String version) { |
| writeStartDocument(null, version); |
| } |
| |
| @Override |
| public void writeStartDocument(String encoding, String version) { |
| final TreeElement treeElement = new TreeElement(null, XMLStreamConstants.START_DOCUMENT, JsonToken.NOT_AVAILABLE); |
| this.treeRoot = treeElement; |
| this.currentTreeElement = treeElement; |
| } |
| |
| @Override |
| public void writeCharacters(String text) { |
| // check for non coalescing read |
| final List<TreeElement> childs = currentTreeElement.childs; |
| if (childs.size() > 0) { |
| final TreeElement child = childs.get(childs.size() - 1); |
| if (child.getXmlEvent() == XMLStreamConstants.CHARACTERS) { |
| child.appendValue(text); |
| |
| return; |
| } |
| } |
| |
| final TreeElement treeElement = new TreeElement(currentTreeElement, XMLStreamConstants.CHARACTERS, JsonToken.VALUE_STRING); |
| treeElement.setValue(text); |
| |
| currentTreeElement.addChild(treeElement); |
| } |
| |
| @Override |
| public void writeCharacters(char[] text, int start, int len) { |
| writeCharacters(new String(text, start, len)); |
| } |
| |
| @Override |
| public String getPrefix(String uri) { |
| return null; |
| } |
| |
| @Override |
| public void setPrefix(String prefix, String uri) { |
| // ignore - ATM we don't care |
| } |
| |
| @Override |
| public void setDefaultNamespace(String uri) { |
| // ignore - ATM we don't care |
| } |
| |
| @Override |
| public NamespaceContext getNamespaceContext() { |
| throw new UnsupportedOperationException(XJConstants.UNSUPPORTED_OPERATION_EXCEPTION_MESSAGE); |
| } |
| |
| @Override |
| public void setNamespaceContext(NamespaceContext context) { |
| // ignore - ATM we don't care |
| } |
| |
| @Override |
| public Object getProperty(String name) throws IllegalArgumentException { |
| throw new IllegalArgumentException(name + " unsupported"); |
| } |
| |
| /** |
| * Class that represents an element of the internal tree |
| */ |
| private static class TreeElement { |
| |
| private TreeElement parent; |
| private List<TreeElement> childs = Collections.emptyList(); |
| |
| private String name; |
| private String value; |
| private int xmlEvent; |
| private JsonToken jsonToken; |
| |
| TreeElement(TreeElement parent, int xmlEvent, String name) { |
| this.parent = parent; |
| this.xmlEvent = xmlEvent; |
| this.name = name; |
| } |
| |
| TreeElement(TreeElement parent, int xmlEvent, JsonToken jsonToken) { |
| this.parent = parent; |
| this.xmlEvent = xmlEvent; |
| this.jsonToken = jsonToken; |
| } |
| |
| TreeElement(TreeElement parent, int xmlEvent, JsonToken jsonToken, String name) { |
| this.parent = parent; |
| this.xmlEvent = xmlEvent; |
| this.jsonToken = jsonToken; |
| this.name = name; |
| } |
| |
| int getXmlEvent() { |
| return this.xmlEvent; |
| } |
| |
| void addChild(TreeElement treeElement) { |
| if (this.childs == Collections.EMPTY_LIST) { |
| this.childs = new ArrayList<>(1); |
| } |
| |
| this.childs.add(treeElement); |
| } |
| |
| void setJsonToken(JsonToken jsonToken) { |
| if (this.jsonToken == null) { |
| this.jsonToken = jsonToken; |
| } |
| } |
| |
| void setName(String name) { |
| this.name = name; |
| } |
| |
| void setValue(String value) { |
| this.value = value; |
| } |
| |
| void appendValue(String value) { |
| this.value += value; |
| } |
| |
| void writeEnd() { |
| if (jsonToken == null) { |
| // no type hints |
| writeEndNoTypeHints(); |
| } else { |
| // type hints given |
| writeEndHaveTypeHints(); |
| } |
| } |
| |
| private void writeEndNoTypeHints() { |
| if (xmlEvent == XMLStreamConstants.START_ELEMENT) { |
| if (childs.isEmpty()) { |
| // empty root element |
| if (this.parent.jsonToken == JsonToken.NOT_AVAILABLE) { |
| jsonToken = JsonToken.START_OBJECT; |
| } else { |
| jsonToken = JsonToken.FIELD_NAME; |
| |
| final TreeElement treeElement = new TreeElement(this, -1, JsonToken.VALUE_STRING); |
| treeElement.setValue(""); |
| this.addChild(treeElement); |
| } |
| } else if (childs.size() == 1 && childs.get(0).xmlEvent == XMLStreamConstants.CHARACTERS) { |
| // just character childs. |
| |
| // empty root element |
| if (this.parent.jsonToken == JsonToken.NOT_AVAILABLE) { |
| jsonToken = JsonToken.START_OBJECT; |
| |
| final TreeElement child = childs.get(0); |
| if (isWhitespace(child.value)) { |
| childs.remove(0); |
| } else { |
| // create new intermediary element |
| final TreeElement treeElement = new TreeElement(this, -1, JsonToken.FIELD_NAME, XJConstants.JSON_WRITER_MIXED_CONTENT_TEXT_KEY); |
| treeElement.addChild(child); |
| childs.set(childs.indexOf(child), treeElement); |
| child.parent = treeElement; |
| } |
| } else { |
| jsonToken = JsonToken.FIELD_NAME; |
| } |
| } else { |
| // mixed content fixup. |
| final Iterator<TreeElement> iterator = childs.iterator(); |
| while (iterator.hasNext()) { |
| TreeElement element = iterator.next(); |
| if (element.jsonToken == JsonToken.VALUE_STRING) { |
| if (isWhitespace(element.value)) { |
| // remove element if is (ignorable-) whitespace |
| iterator.remove(); |
| } else { |
| // create new intermediary element |
| final TreeElement treeElement = new TreeElement(this, -1, JsonToken.FIELD_NAME, |
| element.name != null ? element.name : XJConstants.JSON_WRITER_MIXED_CONTENT_TEXT_KEY); |
| treeElement.addChild(element); |
| childs.set(childs.indexOf(element), treeElement); |
| element.parent = treeElement; |
| element.jsonToken = JsonToken.VALUE_STRING; |
| } |
| } |
| } |
| |
| jsonToken = JsonToken.START_OBJECT; |
| |
| wrapChildsInArrayIfNecessary(); |
| } |
| } else { |
| throw new IllegalStateException("XMLEvent: " + xmlEvent + "; Json Token: " + jsonToken); |
| } |
| } |
| |
| private void writeEndHaveTypeHints() { |
| switch (jsonToken) { |
| case VALUE_NULL: |
| case VALUE_STRING: |
| case VALUE_NUMBER_INT: |
| case VALUE_NUMBER_FLOAT: |
| case VALUE_TRUE: |
| case VALUE_FALSE: |
| if (childs.isEmpty()) { |
| final TreeElement treeElement = new TreeElement(this, -1, jsonToken); |
| treeElement.setValue(""); |
| this.addChild(treeElement); |
| |
| jsonToken = JsonToken.FIELD_NAME; |
| } else if (childs.size() == 1) { |
| childs.get(0).jsonToken = jsonToken; |
| jsonToken = JsonToken.FIELD_NAME; |
| } else { |
| // create FIELD childs if element contains text and attributes. |
| final Iterator<TreeElement> iterator = childs.iterator(); |
| while (iterator.hasNext()) { |
| TreeElement element = iterator.next(); |
| if (isValueToken(element.jsonToken)) { |
| if (isWhitespace(element.value)) { |
| // remove element if is (ignorable-) whitespace |
| iterator.remove(); |
| } else { |
| // create new intermediary element |
| final TreeElement treeElement = new TreeElement(this, -1, JsonToken.FIELD_NAME, |
| element.name != null ? element.name : XJConstants.JSON_WRITER_MIXED_CONTENT_TEXT_KEY); |
| treeElement.addChild(element); |
| childs.set(childs.indexOf(element), treeElement); |
| element.parent = treeElement; |
| if (element.xmlEvent == XMLStreamConstants.CHARACTERS) { |
| element.jsonToken = jsonToken; |
| } |
| } |
| } |
| } |
| |
| jsonToken = JsonToken.START_OBJECT; |
| |
| wrapChildsInArrayIfNecessary(); |
| } |
| break; |
| case START_OBJECT: |
| case START_ARRAY: |
| // mixed content fixup. |
| final Iterator<TreeElement> iterator = childs.iterator(); |
| while (iterator.hasNext()) { |
| TreeElement element = iterator.next(); |
| if (isValueToken(element.jsonToken)) { |
| if (isWhitespace(element.value)) { |
| // remove element if is (ignorable-) whitespace |
| iterator.remove(); |
| } else { |
| // create new intermediary element |
| final TreeElement treeElement = new TreeElement(this, -1, JsonToken.FIELD_NAME, XJConstants.JSON_WRITER_MIXED_CONTENT_TEXT_KEY); |
| treeElement.addChild(element); |
| childs.set(childs.indexOf(element), treeElement); |
| element.parent = treeElement; |
| } |
| } |
| } |
| |
| if (jsonToken != JsonToken.START_ARRAY) { |
| wrapChildsInArrayIfNecessary(); |
| } |
| |
| break; |
| default: |
| throw new IllegalStateException("XMLEvent: " + xmlEvent + "; Json Token: " + jsonToken); |
| } |
| } |
| |
| private void wrapChildsInArrayIfNecessary() { |
| final Map<String, Set<TreeElement>> childElementsMap = childs.stream() |
| .collect(Collectors.groupingBy(o -> o.name, HashMap::new, Collectors.toCollection(LinkedHashSet::new))); |
| |
| // create arrays if element with the same name occurs more than once. |
| for (Map.Entry<String, Set<TreeElement>> mapEntry : childElementsMap.entrySet()) { |
| if (mapEntry.getValue().size() > 1) { |
| if (childElementsMap.size() == 1) { |
| jsonToken = JsonToken.START_ARRAY; |
| } else { |
| final TreeElement treeElement = new TreeElement(this, -1, JsonToken.START_ARRAY, mapEntry.getKey()); |
| treeElement.childs = new ArrayList<>(mapEntry.getValue()); |
| for (TreeElement child : treeElement.childs) { |
| child.parent = treeElement; |
| } |
| |
| final List<TreeElement> newChildList = new ArrayList<>(this.childs.size() - mapEntry.getValue().size() + 1); |
| for (TreeElement e : this.childs) { |
| if (!mapEntry.getValue().contains(e)) { |
| newChildList.add(e); |
| } |
| } |
| childs = newChildList; |
| childs.add(treeElement); |
| } |
| } |
| } |
| } |
| |
| private boolean isWhitespace(String text) { |
| int len = text.length(); |
| int st = 0; |
| |
| while ((st < len) && (text.charAt(st) <= ' ')) { |
| st++; |
| } |
| |
| return st == len; |
| } |
| |
| private boolean isValueToken(JsonToken jsonToken) { |
| return jsonToken == JsonToken.VALUE_STRING |
| || jsonToken == JsonToken.VALUE_NUMBER_FLOAT |
| || jsonToken == JsonToken.VALUE_NUMBER_INT |
| || jsonToken == JsonToken.VALUE_TRUE |
| || jsonToken == JsonToken.VALUE_FALSE |
| || jsonToken == JsonToken.VALUE_NULL; |
| } |
| |
| void write(JsonGenerator jsonGenerator) throws IOException { |
| switch (jsonToken) { |
| case NOT_AVAILABLE: |
| break; |
| case START_OBJECT: |
| if (parent.jsonToken == JsonToken.START_OBJECT) { |
| jsonGenerator.writeObjectFieldStart(name); |
| } else { |
| jsonGenerator.writeStartObject(); |
| } |
| |
| break; |
| case START_ARRAY: |
| if (parent.jsonToken == JsonToken.START_OBJECT) { |
| jsonGenerator.writeArrayFieldStart(name); |
| } else { |
| jsonGenerator.writeStartArray(); |
| } |
| |
| break; |
| case FIELD_NAME: |
| if (parent.jsonToken != JsonToken.START_ARRAY) { |
| jsonGenerator.writeFieldName(name); |
| } |
| |
| break; |
| case VALUE_STRING: |
| jsonGenerator.writeString(value); |
| |
| break; |
| case VALUE_NUMBER_INT: |
| case VALUE_NUMBER_FLOAT: |
| case VALUE_TRUE: |
| case VALUE_FALSE: |
| if (value == null || value.isEmpty()) { |
| jsonGenerator.writeNull(); |
| } else { |
| jsonGenerator.writeRawValue(value); |
| } |
| |
| break; |
| case VALUE_NULL: |
| jsonGenerator.writeNull(); |
| |
| break; |
| default: |
| throw new IllegalStateException("XMLEvent: " + xmlEvent + "; Json Token: " + jsonToken); |
| } |
| |
| for (TreeElement treeElement : childs) { |
| treeElement.write(jsonGenerator); |
| } |
| |
| switch (jsonToken) { |
| case START_OBJECT: |
| jsonGenerator.writeEndObject(); |
| break; |
| case START_ARRAY: |
| jsonGenerator.writeEndArray(); |
| break; |
| case VALUE_NULL: |
| case NOT_AVAILABLE: |
| case FIELD_NAME: |
| case VALUE_STRING: |
| case VALUE_NUMBER_INT: |
| case VALUE_NUMBER_FLOAT: |
| case VALUE_TRUE: |
| case VALUE_FALSE: |
| // nop; |
| break; |
| default: |
| throw new IllegalStateException("XMLEvent: " + xmlEvent + "; Json Token: " + jsonToken); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "TreeElement{" |
| + "name='" + name + '\'' |
| + ", value='" + value + '\'' |
| + ", xmlEvent=" + xmlEvent |
| + ", jsonToken=" + jsonToken |
| + '}'; |
| } |
| } |
| } |