/*
* Copyright 2011, 2012 Odysseus Software GmbH
*
* 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.
*/
package org.apache.synapse.commons.staxon.core.json;
import java.io.IOException;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import org.apache.synapse.commons.staxon.core.base.AbstractXMLStreamReader;
import org.apache.synapse.commons.staxon.core.base.XMLStreamReaderScope;
import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamSource;
import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamSource.Value;
import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamToken;
/**
* JSON XML stream reader.
* <p/>
* <h4>Limitations</h4>
* <ul>
* <li>Mixed content (e.g. <code><alice>bob<edgar/></alice></code>) is not supported.</li>
* </ul>
* <p/>
* <p>The reader may produce processing instructions
* <code><?xml-multiple element-name?></code>
* to indicate array starts (<code>'['</code>).</p>
*/
public class JsonXMLStreamReader extends AbstractXMLStreamReader<JsonXMLStreamReader.ScopeInfo> {
static class ScopeInfo extends JsonXMLStreamScopeInfo {
private String currentTagName;
}
private final JsonStreamSource source;
private final boolean multiplePI;
private final char namespaceSeparator;
private boolean documentArray = false;
/**
* Create reader instance.
*
* @param source stream source
* @param multiplePI whether to produce <code><xml-multiple?></code> PIs to signal array start
* @param namespaceSeparator namespace prefix separator
* @throws XMLStreamException
*/
public JsonXMLStreamReader(JsonStreamSource source, boolean multiplePI, char namespaceSeparator) throws XMLStreamException {
super(new ScopeInfo(), source);
this.source = source;
this.multiplePI = multiplePI;
this.namespaceSeparator = namespaceSeparator;
initialize();
}
private void readStartElementTag(String name) throws XMLStreamException {
int separator = name.indexOf(namespaceSeparator);
if (separator < 0) {
readStartElementTag(XMLConstants.DEFAULT_NS_PREFIX, name, null, new ScopeInfo());
} else {
readStartElementTag(name.substring(0, separator), name.substring(separator + 1), null, new ScopeInfo());
}
}
private void readAttrNsDecl(String name, String value) throws XMLStreamException {
int separator = name.indexOf(namespaceSeparator);
if (separator < 0) {
if (XMLConstants.XMLNS_ATTRIBUTE.equals(name)) {
readNsDecl(XMLConstants.DEFAULT_NS_PREFIX, value);
} else {
readAttr(XMLConstants.DEFAULT_NS_PREFIX, name, null, value);
}
} else {
if (name.startsWith(XMLConstants.XMLNS_ATTRIBUTE) && separator == XMLConstants.XMLNS_ATTRIBUTE.length()) {
readNsDecl(name.substring(separator + 1), value);
} else {
readAttr(name.substring(0, separator), name.substring(separator + 1), null, value);
}
}
}
private void readData(Value value, int type) throws XMLStreamException {
readData(value.text, value.data, type);
}
private void consumeName(ScopeInfo info) throws XMLStreamException, IOException {
String fieldName = source.name();
if (fieldName.startsWith("@")) {
fieldName = fieldName.substring(1);
if (source.peek() == JsonStreamToken.VALUE) {
readAttrNsDecl(fieldName, source.value().text);
} else if (XMLConstants.XMLNS_ATTRIBUTE.equals(fieldName)) { // badgerfish
source.startObject();
while (source.peek() == JsonStreamToken.NAME) {
String prefix = source.name();
if ("$".equals(prefix)) {
readNsDecl(XMLConstants.DEFAULT_NS_PREFIX, source.value().text);
} else {
readNsDecl(prefix, source.value().text);
}
}
source.endObject();
} else {
throw new IllegalStateException("Expected attribute value");
}
} else if ("$".equals(fieldName)) {
readData(source.value(), XMLStreamConstants.CHARACTERS);
} else {
info.currentTagName = fieldName;
}
}
@Override
protected boolean consume() throws XMLStreamException, IOException {
XMLStreamReaderScope<ScopeInfo> scope = getScope();
switch (source.peek()) {
case NAME:
consumeName(scope.getInfo());
return consume();
case START_ARRAY:
source.startArray();
if (scope.getInfo().isArray()) {
throw new IOException("Array start inside array");
}
if (scope.isRoot() && !isStartDocumentRead()) {
documentArray = true;
} else {
if (scope.getInfo().currentTagName == null) {
throw new IOException("Array name missing");
}
scope.getInfo().startArray(scope.getInfo().currentTagName);
}
if (multiplePI) {
readPI(JsonXMLStreamConstants.MULTIPLE_PI_TARGET, scope.getInfo().currentTagName);
}
return consume();
case START_OBJECT:
source.startObject();
if (scope.isRoot() && !isStartDocumentRead()) {
readStartDocument(null, null, null);
} else {
if (scope.getInfo().isArray()) {
scope.getInfo().incArraySize();
}
if (scope.getInfo().currentTagName != null) {
readStartElementTag(scope.getInfo().currentTagName);
}
}
return consume();
case END_OBJECT:
source.endObject();
if (scope.isRoot() && isStartDocumentRead()) {
readEndDocument();
return documentArray;
} else {
readEndElementTag();
return true;
}
case VALUE:
String name = scope.getInfo().currentTagName;
if (scope.getInfo().isArray()) {
scope.getInfo().incArraySize();
name = scope.getInfo().getArrayName();
}
if (getScope().isRoot() && !isStartDocumentRead()) { // hack: allow to read simple value
readData(source.value(), XMLStreamConstants.CHARACTERS);
} else {
readStartElementTag(name);
Value value = source.value();
if (value != JsonStreamSource.NULL) {
readData(value, XMLStreamConstants.CHARACTERS);
}
readEndElementTag();
}
return true;
case END_ARRAY:
source.endArray();
if (getScope().isRoot() && documentArray) {
return false;
}
if (!scope.getInfo().isArray()) {
throw new IllegalStateException("Array end without matching start");
}
scope.getInfo().endArray();
return true;
case NONE:
return false;
default:
throw new IOException("Unexpected token: " + source.peek());
}
}
/**
* @return <code>true</code> iff the current event data is a number primitive
*/
public boolean hasNumber() {
return getEventData() instanceof Number;
}
/**
* @return number primitive
* @throws ClassCastException
*/
public Number getNumber() {
return (Number) getEventData();
}
/**
* @return <code>true</code> iff the current event data is a boolean primitive
*/
public boolean hasBoolean() {
return getEventData() instanceof Boolean;
}
/**
* @return boolean primitive
* @throws ClassCastException
*/
public Boolean getBoolean() {
return (Boolean) getEventData();
}
@Override
public void close() throws XMLStreamException {
super.close();
try {
source.close();
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
}