![JAR search and dependency download from the Maven repository](/logo.png)
org.apache.juneau.xml.XmlParserSession Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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.juneau.xml;
import static javax.xml.stream.XMLStreamConstants.*;
import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static org.apache.juneau.xml.annotation.XmlFormat.*;
import java.io.IOException;
import java.lang.reflect.*;
import java.nio.charset.*;
import java.util.*;
import java.util.function.*;
import javax.xml.stream.*;
import javax.xml.stream.util.*;
import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.httppart.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.swap.*;
import org.apache.juneau.xml.annotation.*;
/**
* Session object that lives for the duration of a single use of {@link XmlParser}.
*
* Notes:
* - This class is not thread safe and is typically discarded after one use.
*
*
* See Also:
* - XML Details
*
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class XmlParserSession extends ReaderParserSession {
//-------------------------------------------------------------------------------------------------------------------
// Static
//-------------------------------------------------------------------------------------------------------------------
private static final int UNKNOWN=0, OBJECT=1, ARRAY=2, STRING=3, NUMBER=4, BOOLEAN=5, NULL=6;
/**
* Creates a new builder for this object.
*
* @param ctx The context creating this session.
* @return A new builder.
*/
public static Builder create(XmlParser ctx) {
return new Builder(ctx);
}
//-------------------------------------------------------------------------------------------------------------------
// Builder
//-------------------------------------------------------------------------------------------------------------------
/**
* Builder class.
*/
@FluentSetters
public static class Builder extends ReaderParserSession.Builder {
XmlParser ctx;
/**
* Constructor
*
* @param ctx The context creating this session.
*/
protected Builder(XmlParser ctx) {
super(ctx);
this.ctx = ctx;
}
@Override
public XmlParserSession build() {
return new XmlParserSession(this);
}
//
@Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
public Builder apply(Class type, Consumer apply) {
super.apply(type, apply);
return this;
}
@Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
public Builder debug(Boolean value) {
super.debug(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
public Builder properties(Map value) {
super.properties(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
public Builder property(String key, Object value) {
super.property(key, value);
return this;
}
@Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
public Builder unmodifiable() {
super.unmodifiable();
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
public Builder locale(Locale value) {
super.locale(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
public Builder localeDefault(Locale value) {
super.localeDefault(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
public Builder mediaType(MediaType value) {
super.mediaType(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
public Builder mediaTypeDefault(MediaType value) {
super.mediaTypeDefault(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
public Builder timeZone(TimeZone value) {
super.timeZone(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
public Builder timeZoneDefault(TimeZone value) {
super.timeZoneDefault(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
public Builder javaMethod(Method value) {
super.javaMethod(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
public Builder outer(Object value) {
super.outer(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
public Builder schema(HttpPartSchema value) {
super.schema(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.parser.ParserSession.Builder */
public Builder schemaDefault(HttpPartSchema value) {
super.schemaDefault(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */
public Builder fileCharset(Charset value) {
super.fileCharset(value);
return this;
}
@Override /* GENERATED - org.apache.juneau.parser.ReaderParserSession.Builder */
public Builder streamCharset(Charset value) {
super.streamCharset(value);
return this;
}
//
}
//-------------------------------------------------------------------------------------------------------------------
// Instance
//-------------------------------------------------------------------------------------------------------------------
private final XmlParser ctx;
private final StringBuilder rsb = new StringBuilder(); // Reusable string builder used in this class.
/**
* Constructor.
*
* @param builder The builder for this object.
*/
protected XmlParserSession(Builder builder) {
super(builder);
ctx = builder.ctx;
}
/**
* Wrap the specified reader in a STAX reader based on settings in this context.
*
* @param pipe The parser input.
* @return The new STAX reader.
* @throws IOException Thrown by underlying stream.
* @throws XMLStreamException Unexpected XML processing error.
*/
protected final XmlReader getXmlReader(ParserPipe pipe) throws IOException, XMLStreamException {
return new XmlReader(pipe, isValidating(), getReporter(), getResolver(), getEventAllocator());
}
/**
* Decodes and trims the specified string.
*
*
* Any '_x####_' sequences in the string will be decoded.
*
* @param s The string to be decoded.
* @return The decoded string.
*/
protected final String decodeString(String s) {
if (s == null)
return null;
rsb.setLength(0);
s = XmlUtils.decode(s, rsb);
if (isTrimStrings())
s = s.trim();
return s;
}
/*
* Returns the name of the current XML element.
* Any '_x####_' sequences in the string will be decoded.
*/
private String getElementName(XmlReader r) {
return decodeString(r.getLocalName());
}
/*
* Returns the _name attribute value.
* Any '_x####_' sequences in the string will be decoded.
*/
private String getNameProperty(XmlReader r) {
return decodeString(r.getAttributeValue(null, getNamePropertyName()));
}
/*
* Returns the name of the specified attribute on the current XML element.
* Any '_x####_' sequences in the string will be decoded.
*/
private String getAttributeName(XmlReader r, int i) {
return decodeString(r.getAttributeLocalName(i));
}
/*
* Returns the value of the specified attribute on the current XML element.
* Any '_x####_' sequences in the string will be decoded.
*/
private String getAttributeValue(XmlReader r, int i) {
return decodeString(r.getAttributeValue(i));
}
/**
* Returns the text content of the current XML element.
*
*
* Any '_x####_' sequences in the string will be decoded.
*
*
* Leading and trailing whitespace (unencoded) will be trimmed from the result.
*
* @param r The reader to read the element text from.
* @return The decoded text. null if the text consists of the sequence '_x0000_' .
* @throws XMLStreamException Thrown by underlying reader.
* @throws IOException Thrown by underlying stream.
* @throws ParseException Malformed input encountered.
*/
protected String getElementText(XmlReader r) throws XMLStreamException, IOException, ParseException {
return decodeString(r.getElementText().trim());
}
/*
* Returns the content of the current CHARACTERS node.
* Any '_x####_' sequences in the string will be decoded.
* Leading and trailing whitespace (unencoded) will be trimmed from the result.
*/
private String getText(XmlReader r, boolean trim) {
String s = r.getText();
if (trim)
s = s.trim();
if (s.isEmpty())
return null;
return decodeString(s);
}
/*
* Shortcut for calling getText(r, true );
.
*/
private String getText(XmlReader r) {
return getText(r, true);
}
/*
* Takes the element being read from the XML stream reader and reconstructs it as XML.
* Used when reconstructing bean properties of type {@link XmlFormat#XMLTEXT}.
*/
private String getElementAsString(XmlReader r) {
int t = r.getEventType();
if (t > 2)
throw new BasicRuntimeException("Invalid event type on stream reader for elementToString() method: ''{0}''", XmlUtils.toReadableEvent(r));
rsb.setLength(0);
rsb.append("<").append(t == 1 ? "" : "/").append(r.getLocalName());
if (t == 1)
for (int i = 0; i < r.getAttributeCount(); i++)
rsb.append(' ').append(r.getAttributeName(i)).append('=').append('\'').append(r.getAttributeValue(i)).append('\'');
rsb.append('>');
return rsb.toString();
}
/**
* Parses the current element as text.
*
* @param r The input reader.
* @return The parsed text.
* @throws XMLStreamException Thrown by underlying reader.
* @throws IOException Thrown by underlying stream.
* @throws ParseException Malformed input encountered.
*/
protected String parseText(XmlReader r) throws IOException, XMLStreamException, ParseException {
// Note that this is different than {@link #getText(XmlReader)} since it assumes that we're pointing to a
// whitespace element.
StringBuilder sb2 = getStringBuilder();
int depth = 0;
while (true) {
int et = r.getEventType();
if (et == START_ELEMENT) {
sb2.append(getElementAsString(r));
depth++;
} else if (et == CHARACTERS) {
sb2.append(getText(r));
} else if (et == END_ELEMENT) {
sb2.append(getElementAsString(r));
depth--;
if (depth <= 0)
break;
}
et = r.next();
}
String s = sb2.toString();
returnStringBuilder(sb2);
return s;
}
/**
* Returns true if the current element is a whitespace element.
*
*
* For the XML parser, this always returns false .
* However, the HTML parser defines various whitespace elements such as "br" and "sp" .
*
* @param r The XML stream reader to read the current event from.
* @return true if the current element is a whitespace element.
*/
protected boolean isWhitespaceElement(XmlReader r) {
return false;
}
/**
* Parses the current whitespace element.
*
*
* For the XML parser, this always returns null since there is no concept of a whitespace element.
* However, the HTML parser defines various whitespace elements such as "br" and "sp" .
*
* @param r The XML stream reader to read the current event from.
* @return The whitespace character or characters.
* @throws XMLStreamException Thrown by underlying reader.
* @throws IOException Thrown by underlying stream.
* @throws ParseException Malformed input encountered.
*/
protected String parseWhitespaceElement(XmlReader r) throws IOException, XMLStreamException, ParseException {
return null;
}
@Override /* ParserSession */
protected T doParse(ParserPipe pipe, ClassMeta type) throws IOException, ParseException, ExecutableException {
try {
return parseAnything(type, null, getXmlReader(pipe), getOuter(), true, null);
} catch (XMLStreamException e) {
throw new ParseException(e);
}
}
@Override /* ReaderParserSession */
protected Map doParseIntoMap(ParserPipe pipe, Map m, Type keyType, Type valueType) throws Exception {
ClassMeta cm = getClassMeta(m.getClass(), keyType, valueType);
return parseIntoMap(pipe, m, cm.getKeyType(), cm.getValueType());
}
@Override /* ReaderParserSession */
protected Collection doParseIntoCollection(ParserPipe pipe, Collection c, Type elementType) throws Exception {
ClassMeta cm = getClassMeta(c.getClass(), elementType);
return parseIntoCollection(pipe, c, cm.getElementType());
}
/**
* Workhorse method.
*
* @param The expected type of object.
* @param eType The expected type of object.
* @param currAttr The current bean property name.
* @param r The reader.
* @param outer The outer object.
* @param isRoot If true , then we're serializing a root element in the document.
* @param pMeta The bean property metadata.
* @return The parsed object.
* @throws IOException Thrown by underlying stream.
* @throws ParseException Malformed input encountered.
* @throws ExecutableException Exception occurred on invoked constructor/method/field.
* @throws XMLStreamException Malformed XML encountered.
*/
protected T parseAnything(ClassMeta eType, String currAttr, XmlReader r,
Object outer, boolean isRoot, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
if (eType == null)
eType = (ClassMeta)object();
ObjectSwap swap = (ObjectSwap)eType.getSwap(this);
BuilderSwap builder = (BuilderSwap)eType.getBuilderSwap(this);
ClassMeta> sType = null;
if (builder != null)
sType = builder.getBuilderClassMeta(this);
else if (swap != null)
sType = swap.getSwapClassMeta(this);
else
sType = eType;
if (sType.isOptional())
return (T)optional(parseAnything(eType.getElementType(), currAttr, r, outer, isRoot, pMeta));
setCurrentClass(sType);
String wrapperAttr = (isRoot && isPreserveRootElement()) ? r.getName().getLocalPart() : null;
String typeAttr = r.getAttributeValue(null, getBeanTypePropertyName(eType));
boolean isNil = "true".equals(r.getAttributeValue(null, "nil"));
int jsonType = getJsonType(typeAttr);
String elementName = getElementName(r);
if (jsonType == 0) {
if (elementName == null || elementName.equals(currAttr))
jsonType = UNKNOWN;
else {
typeAttr = elementName;
jsonType = getJsonType(elementName);
}
}
ClassMeta tcm = getClassMeta(typeAttr, pMeta, eType);
if (tcm == null && elementName != null && ! elementName.equals(currAttr))
tcm = getClassMeta(elementName, pMeta, eType);
if (tcm != null)
sType = eType = tcm;
Object o = null;
if (jsonType == NULL) {
r.nextTag(); // Discard end tag
return null;
}
if (sType.isObject()) {
if (jsonType == OBJECT) {
JsonMap m = new JsonMap(this);
parseIntoMap(r, m, string(), object(), pMeta);
if (wrapperAttr != null)
m = new JsonMap(this).append(wrapperAttr, m);
o = cast(m, pMeta, eType);
} else if (jsonType == ARRAY)
o = parseIntoCollection(r, new JsonList(this), null, pMeta);
else if (jsonType == STRING) {
o = getElementText(r);
if (sType.isChar())
o = parseCharacter(o);
}
else if (jsonType == NUMBER)
o = parseNumber(getElementText(r), null);
else if (jsonType == BOOLEAN)
o = Boolean.parseBoolean(getElementText(r));
else if (jsonType == UNKNOWN)
o = getUnknown(r);
} else if (sType.isBoolean()) {
o = Boolean.parseBoolean(getElementText(r));
} else if (sType.isCharSequence()) {
o = getElementText(r);
} else if (sType.isChar()) {
o = parseCharacter(getElementText(r));
} else if (sType.isMap()) {
Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
o = parseIntoMap(r, m, sType.getKeyType(), sType.getValueType(), pMeta);
if (wrapperAttr != null)
o = new JsonMap(this).append(wrapperAttr, m);
} else if (sType.isCollection()) {
Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance(outer) : new JsonList(this));
o = parseIntoCollection(r, l, sType, pMeta);
} else if (sType.isNumber()) {
o = parseNumber(getElementText(r), (Class extends Number>)sType.getInnerClass());
} else if (builder != null || sType.canCreateNewBean(outer)) {
if (getXmlClassMeta(sType).getFormat() == COLLAPSED) {
String fieldName = r.getLocalName();
BeanMap> m = builder != null ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.getInnerClass());
BeanPropertyMeta bpm = getXmlBeanMeta(m.getMeta()).getPropertyMeta(fieldName);
ClassMeta> cm = m.getMeta().getClassMeta();
Object value = parseAnything(cm, currAttr, r, m.getBean(false), false, null);
setName(cm, value, currAttr);
bpm.set(m, currAttr, value);
o = builder != null ? builder.build(this, m.getBean(), eType) : m.getBean();
} else {
BeanMap m = builder != null ? toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.getInnerClass());
m = parseIntoBean(r, m, isNil);
o = builder != null ? builder.build(this, m.getBean(), eType) : m.getBean();
}
} else if (sType.isArray() || sType.isArgs()) {
ArrayList l = (ArrayList)parseIntoCollection(r, list(), sType, pMeta);
o = toArray(sType, l);
} else if (sType.canCreateNewInstanceFromString(outer)) {
o = sType.newInstanceFromString(outer, getElementText(r));
} else if (sType.getProxyInvocationHandler() != null) {
JsonMap m = new JsonMap(this);
parseIntoMap(r, m, string(), object(), pMeta);
if (wrapperAttr != null)
m = new JsonMap(this).append(wrapperAttr, m);
o = newBeanMap(outer, sType.getInnerClass()).load(m).getBean();
} else {
throw new ParseException(this,
"Class ''{0}'' could not be instantiated. Reason: ''{1}'', property: ''{2}''",
sType.getInnerClass().getName(), sType.getNotABeanReason(), pMeta == null ? null : pMeta.getName());
}
if (swap != null && o != null)
o = unswap(swap, o, eType);
if (outer != null)
setParent(eType, o, outer);
return (T)o;
}
private Map parseIntoMap(XmlReader r, Map m, ClassMeta keyType,
ClassMeta valueType, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
int depth = 0;
for (int i = 0; i < r.getAttributeCount(); i++) {
String a = r.getAttributeLocalName(i);
// TODO - Need better handling of namespaces here.
if (! isSpecialAttr(a)) {
K key = trim(convertAttrToType(m, a, keyType));
V value = trim(convertAttrToType(m, r.getAttributeValue(i), valueType));
setName(valueType, value, key);
m.put(key, value);
}
}
do {
int event = r.nextTag();
String currAttr;
if (event == START_ELEMENT) {
depth++;
currAttr = getNameProperty(r);
if (currAttr == null)
currAttr = getElementName(r);
K key = convertAttrToType(m, currAttr, keyType);
V value = parseAnything(valueType, currAttr, r, m, false, pMeta);
setName(valueType, value, currAttr);
if (valueType.isObject() && m.containsKey(key)) {
Object o = m.get(key);
if (o instanceof List)
((List)o).add(value);
else
m.put(key, (V)new JsonList(o, value).setBeanSession(this));
} else {
m.put(key, value);
}
} else if (event == END_ELEMENT) {
depth--;
return m;
}
} while (depth > 0);
return m;
}
private Collection parseIntoCollection(XmlReader r, Collection l,
ClassMeta> type, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException, XMLStreamException {
int depth = 0;
int argIndex = 0;
do {
int event = r.nextTag();
if (event == START_ELEMENT) {
depth++;
ClassMeta> elementType = type == null ? object() : type.isArgs() ? type.getArg(argIndex++) : type.getElementType();
E value = (E)parseAnything(elementType, null, r, l, false, pMeta);
l.add(value);
} else if (event == END_ELEMENT) {
depth--;
return l;
}
} while (depth > 0);
return l;
}
private static int getJsonType(String s) {
if (s == null)
return UNKNOWN;
char c = s.charAt(0);
switch(c) {
case 'o': return (s.equals("object") ? OBJECT : UNKNOWN);
case 'a': return (s.equals("array") ? ARRAY : UNKNOWN);
case 's': return (s.equals("string") ? STRING : UNKNOWN);
case 'b': return (s.equals("boolean") ? BOOLEAN : UNKNOWN);
case 'n': {
c = s.charAt(2);
switch(c) {
case 'm': return (s.equals("number") ? NUMBER : UNKNOWN);
case 'l': return (s.equals("null") ? NULL : UNKNOWN);
}
//return NUMBER;
}
}
return UNKNOWN;
}
private BeanMap parseIntoBean(XmlReader r, BeanMap m, boolean isNil) throws IOException, ParseException, ExecutableException, XMLStreamException {
BeanMeta> bMeta = m.getMeta();
XmlBeanMeta xmlMeta = getXmlBeanMeta(bMeta);
for (int i = 0; i < r.getAttributeCount(); i++) {
String key = getAttributeName(r, i);
if (! ("nil".equals(key) || isSpecialAttr(key))) {
String val = r.getAttributeValue(i);
String ns = r.getAttributeNamespace(i);
BeanPropertyMeta bpm = xmlMeta.getPropertyMeta(key);
if (bpm == null) {
if (xmlMeta.getAttrsProperty() != null) {
xmlMeta.getAttrsProperty().add(m, key, key, val);
} else if (ns == null) {
onUnknownProperty(key, m, val);
}
} else {
try {
bpm.set(m, key, val);
} catch (BeanRuntimeException e) {
onBeanSetterException(bpm, e);
throw e;
}
}
}
}
BeanPropertyMeta cp = xmlMeta.getContentProperty();
XmlFormat cpf = xmlMeta.getContentFormat();
boolean trim = cp == null || ! cpf.isOneOf(MIXED_PWS, TEXT_PWS);
ClassMeta> cpcm = (cp == null ? object() : cp.getClassMeta());
StringBuilder sb = null;
BeanRegistry breg = cp == null ? null : cp.getBeanRegistry();
LinkedList