XmlTokenBuffer.java
package tools.jackson.dataformat.xml.deser;
import java.util.Set;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.ObjectReadContext;
import tools.jackson.core.SerializableString;
import tools.jackson.core.sym.PropertyNameMatcher;
import tools.jackson.core.util.JsonParserDelegate;
import tools.jackson.databind.util.TokenBuffer;
/**
* XML-specific {@link TokenBuffer} sub-class that ensures parsers created
* from buffered content implement {@link ElementWrappable}, allowing
* virtual wrapping to be configured even when content has been buffered
* (e.g., during polymorphic type resolution or {@code @JsonUnwrapped} handling).
*
* @since 3.2
*/
public class XmlTokenBuffer extends TokenBuffer
{
/**
* Reference to the original XML parser that implements {@link ElementWrappable},
* if one was found when this buffer was created.
*/
protected final ElementWrappable _wrappableParser;
protected XmlTokenBuffer(JsonParser p, ObjectReadContext ctxt)
{
super(p, ctxt);
// Find the ElementWrappable parser by unwrapping delegates
JsonParser unwrapped = p;
while (unwrapped instanceof JsonParserDelegate del) {
unwrapped = del.delegate();
}
_wrappableParser = (unwrapped instanceof ElementWrappable ew) ? ew : null;
}
public static XmlTokenBuffer xmlBufferForInputBuffering(JsonParser p,
ObjectReadContext ctxt) {
return new XmlTokenBuffer(p, ctxt);
}
/*
/**********************************************************************
/* Parser construction overrides
/**********************************************************************
*/
@Override
public JsonParser asParser(ObjectReadContext readCtxt)
{
return _wrapIfNeeded(super.asParser(readCtxt));
}
@Override
public JsonParser asParser(ObjectReadContext readCtxt, JsonParser p0)
{
return _wrapIfNeeded(super.asParser(readCtxt, p0));
}
protected JsonParser _wrapIfNeeded(JsonParser p) {
return (_wrappableParser == null) ? p
: new ElementWrappableParser(p, _wrappableParser);
}
/*
/**********************************************************************
/* Helper classes
/**********************************************************************
*/
/**
* Parser delegate that implements {@link ElementWrappable} to support
* virtual wrapping on buffered token streams. For buffered content
* (e.g., from {@code @JsonUnwrapped} handling), this parser injects
* virtual {@code START_ARRAY}/{@code END_ARRAY} tokens around consecutive
* properties with the same name that need wrapping, transforming
* repeated XML elements into a JSON array structure.
*<p>
* For non-buffered content (e.g., polymorphic type resolution where
* the XML parser is still active), wrapping is delegated to the
* original XML parser.
*/
static class ElementWrappableParser extends JsonParserDelegate
implements ElementWrappable
{
// State machine constants
private static final int STATE_NORMAL = 0;
private static final int STATE_EMIT_START_ARRAY = 1;
private static final int STATE_WRAPPING = 2;
private static final int STATE_EMIT_PENDING = 3;
protected final ElementWrappable _wrappable;
// Virtual wrapping configuration
protected Set<String> _namesToWrap;
protected boolean _caseInsensitive;
/**
* Whether local wrapping is active. When true, this parser injects
* virtual array tokens into the buffered token stream for names that
* need wrapping. Set when {@link #addVirtualWrapping} is called.
*/
protected boolean _localWrapping;
private int _wrapState = STATE_NORMAL;
private String _currentWrapName;
private int _wrapDepth;
// Pending token to emit after END_ARRAY
private JsonToken _pendingToken;
private String _pendingName;
// Override for currentName/currentToken when emitting virtual tokens
private JsonToken _virtualToken;
private String _virtualName;
ElementWrappableParser(JsonParser delegate, ElementWrappable wrappable) {
super(delegate);
_wrappable = wrappable;
}
@Override
public void addVirtualWrapping(Set<String> namesToWrap, boolean caseInsensitive) {
_namesToWrap = namesToWrap;
_caseInsensitive = caseInsensitive;
// Always delegate to the original XML parser: this handles the
// polymorphic type resolution case where the parser is still live
// and will read remaining (unbuffered) content.
_wrappable.addVirtualWrapping(namesToWrap, caseInsensitive);
// Also enable local wrapping on the buffered tokens: this handles
// the @JsonUnwrapped case where all content is fully buffered and
// the original parser has moved past it.
_localWrapping = true;
}
private boolean _shouldWrap(String name) {
if (_namesToWrap == null) {
return false;
}
if (_caseInsensitive) {
for (String n : _namesToWrap) {
if (n.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
return _namesToWrap.contains(name);
}
@Override
public JsonToken currentToken() {
if (_virtualToken != null) {
return _virtualToken;
}
return delegate.currentToken();
}
@Override
public int currentTokenId() {
final JsonToken t = currentToken();
return (t == null) ? JsonToken.NOT_AVAILABLE.id() : t.id();
}
@Override
public String currentName() {
if (_virtualName != null) {
return _virtualName;
}
return delegate.currentName();
}
@Override
public boolean isExpectedStartArrayToken() {
return currentToken() == JsonToken.START_ARRAY;
}
@Override
public boolean isExpectedStartObjectToken() {
return currentToken() == JsonToken.START_OBJECT;
}
@Override
public boolean hasToken(JsonToken t) {
return currentToken() == t;
}
@Override
public boolean hasTokenId(int id) {
final JsonToken t = currentToken();
return (t != null) && (t.id() == id);
}
@Override
public boolean hasCurrentToken() {
return currentToken() != null;
}
@Override
public JsonToken nextToken() throws JacksonException {
if (!_localWrapping) {
return delegate.nextToken();
}
return _nextTokenWrapping();
}
@Override
public JsonToken nextValue() throws JacksonException {
if (!_localWrapping) {
return delegate.nextValue();
}
JsonToken t = nextToken();
if (t == JsonToken.PROPERTY_NAME) {
t = nextToken();
}
return t;
}
@Override
public String nextName() throws JacksonException {
if (!_localWrapping) {
return delegate.nextName();
}
return (nextToken() == JsonToken.PROPERTY_NAME) ? currentName() : null;
}
@Override
public boolean nextName(SerializableString str) throws JacksonException {
if (!_localWrapping) {
return delegate.nextName(str);
}
return (nextToken() == JsonToken.PROPERTY_NAME) && str.getValue().equals(currentName());
}
@Override
public int nextNameMatch(PropertyNameMatcher matcher) throws JacksonException {
if (!_localWrapping) {
return delegate.nextNameMatch(matcher);
}
String str = nextName();
if (str != null) {
return matcher.matchName(str);
}
if (hasToken(JsonToken.END_OBJECT)) {
return PropertyNameMatcher.MATCH_END_OBJECT;
}
return PropertyNameMatcher.MATCH_ODD_TOKEN;
}
@Override
public int currentNameMatch(PropertyNameMatcher matcher) {
if (!_localWrapping || _virtualToken == null) {
return delegate.currentNameMatch(matcher);
}
if (_virtualToken == JsonToken.PROPERTY_NAME && _virtualName != null) {
return matcher.matchName(_virtualName);
}
if (_virtualToken == JsonToken.END_OBJECT) {
return PropertyNameMatcher.MATCH_END_OBJECT;
}
return PropertyNameMatcher.MATCH_ODD_TOKEN;
}
private JsonToken _nextTokenWrapping() throws JacksonException {
switch (_wrapState) {
case STATE_EMIT_START_ARRAY:
_wrapState = STATE_WRAPPING;
_wrapDepth = 0;
_virtualToken = JsonToken.START_ARRAY;
_virtualName = null;
return JsonToken.START_ARRAY;
case STATE_EMIT_PENDING:
JsonToken pt = _pendingToken;
String pn = _pendingName;
_pendingToken = null;
_pendingName = null;
_virtualToken = pt;
_virtualName = pn;
// Check if pending field is also a wrapped name
if (pt == JsonToken.PROPERTY_NAME && _shouldWrap(pn)) {
_currentWrapName = pn;
_wrapState = STATE_EMIT_START_ARRAY;
} else {
_wrapState = STATE_NORMAL;
}
return pt;
case STATE_WRAPPING:
return _nextWrapping();
default: // STATE_NORMAL
return _nextNormal();
}
}
private JsonToken _nextNormal() throws JacksonException {
_virtualToken = null;
_virtualName = null;
JsonToken t = delegate.nextToken();
if (t == JsonToken.PROPERTY_NAME) {
String name = delegate.currentName();
if (_shouldWrap(name)) {
_currentWrapName = name;
_wrapState = STATE_EMIT_START_ARRAY;
// Return the PROPERTY_NAME, next call will emit START_ARRAY
}
}
return t;
}
private JsonToken _nextWrapping() throws JacksonException {
// When inside nested content (depth > 0), just pass through
if (_wrapDepth > 0) {
_virtualToken = null;
_virtualName = null;
JsonToken t = delegate.nextToken();
if (t == JsonToken.START_OBJECT || t == JsonToken.START_ARRAY) {
++_wrapDepth;
} else if (t == JsonToken.END_OBJECT || t == JsonToken.END_ARRAY) {
--_wrapDepth;
}
return t;
}
// At wrapping level (depth == 0)
_virtualToken = null;
_virtualName = null;
JsonToken t = delegate.nextToken();
if (t == null) {
// Unexpected end of buffer while in array ��� close the virtual array
_currentWrapName = null;
_wrapState = STATE_NORMAL;
_virtualToken = JsonToken.END_ARRAY;
return JsonToken.END_ARRAY;
}
if (t == JsonToken.PROPERTY_NAME) {
String name = delegate.currentName();
if (_matchesWrapName(name)) {
// Same collection element - skip the duplicate field name,
// return the value token directly
t = delegate.nextToken();
if (t == JsonToken.START_OBJECT || t == JsonToken.START_ARRAY) {
++_wrapDepth;
}
return t;
}
// Different field - end the virtual array, save this token as pending
_pendingToken = t;
_pendingName = name;
_currentWrapName = null;
_wrapState = STATE_EMIT_PENDING;
_virtualToken = JsonToken.END_ARRAY;
_virtualName = null;
return JsonToken.END_ARRAY;
}
if (t == JsonToken.END_OBJECT) {
// End of containing object - end array, save END_OBJECT as pending
_pendingToken = t;
_pendingName = null;
_currentWrapName = null;
_wrapState = STATE_EMIT_PENDING;
_virtualToken = JsonToken.END_ARRAY;
_virtualName = null;
return JsonToken.END_ARRAY;
}
// Other tokens (shouldn't normally happen at depth 0 in wrapping,
// but handle gracefully)
if (t == JsonToken.START_OBJECT || t == JsonToken.START_ARRAY) {
++_wrapDepth;
}
return t;
}
private boolean _matchesWrapName(String name) {
if (_caseInsensitive) {
return _currentWrapName.equalsIgnoreCase(name);
}
return _currentWrapName.equals(name);
}
}
}