Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make empty tags configurable during serialization #640

Open
wants to merge 6 commits into
base: 2.17
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public class XmlFactory extends JsonFactory

protected String _cfgNameForTextElement;

protected String _cfgValueForEmptyElement;

protected XmlNameProcessor _nameProcessor;

/*
Expand Down Expand Up @@ -107,18 +109,26 @@ public XmlFactory(ObjectCodec oc, XMLInputFactory xmlIn, XMLOutputFactory xmlOut
public XmlFactory(ObjectCodec oc, int xpFeatures, int xgFeatures,
XMLInputFactory xmlIn, XMLOutputFactory xmlOut,
String nameForTextElem) {
this(oc, xpFeatures, xgFeatures, xmlIn, xmlOut, nameForTextElem, XmlNameProcessors.newPassthroughProcessor());
this(oc, xpFeatures, xgFeatures, xmlIn, xmlOut, nameForTextElem, FromXmlParser.DEFAULT_EMPTY_ELEMENT_VALUE, XmlNameProcessors.newPassthroughProcessor());
}

public XmlFactory(ObjectCodec oc, int xpFeatures, int xgFeatures,
XMLInputFactory xmlIn, XMLOutputFactory xmlOut,
String nameForTextElem, String valueForEmptyElement) {
this(oc, xpFeatures, xgFeatures, xmlIn, xmlOut, nameForTextElem, valueForEmptyElement,
XmlNameProcessors.newPassthroughProcessor());
}

protected XmlFactory(ObjectCodec oc, int xpFeatures, int xgFeatures,
XMLInputFactory xmlIn, XMLOutputFactory xmlOut,
String nameForTextElem, XmlNameProcessor nameProcessor)
String nameForTextElem, String valueForEmptyElement, XmlNameProcessor nameProcessor)
{
super(oc);
_nameProcessor = nameProcessor;
_xmlParserFeatures = xpFeatures;
_xmlGeneratorFeatures = xgFeatures;
_cfgNameForTextElement = nameForTextElem;
_cfgValueForEmptyElement = valueForEmptyElement;
if (xmlIn == null) {
xmlIn = StaxUtil.defaultInputFactory(getClass().getClassLoader());
// as per [dataformat-xml#190], disable external entity expansion by default
Expand All @@ -145,6 +155,7 @@ protected XmlFactory(XmlFactory src, ObjectCodec oc)
_xmlParserFeatures = src._xmlParserFeatures;
_xmlGeneratorFeatures = src._xmlGeneratorFeatures;
_cfgNameForTextElement = src._cfgNameForTextElement;
_cfgValueForEmptyElement = src._cfgValueForEmptyElement;
_xmlInputFactory = src._xmlInputFactory;
_xmlOutputFactory = src._xmlOutputFactory;
_nameProcessor = src._nameProcessor;
Expand All @@ -161,6 +172,7 @@ protected XmlFactory(XmlFactoryBuilder b)
_xmlParserFeatures = b.formatParserFeaturesMask();
_xmlGeneratorFeatures = b.formatGeneratorFeaturesMask();
_cfgNameForTextElement = b.nameForTextElement();
_cfgValueForEmptyElement = b.valueForEmptyElement();
_xmlInputFactory = b.xmlInputFactory();
_xmlOutputFactory = b.xmlOutputFactory();
_nameProcessor = b.xmlNameProcessor();
Expand Down Expand Up @@ -237,7 +249,7 @@ protected Object readResolve() {
throw new IllegalArgumentException(e);
}
return new XmlFactory(_objectCodec, _xmlParserFeatures, _xmlGeneratorFeatures,
inf, outf, _cfgNameForTextElement);
inf, outf, _cfgNameForTextElement, _cfgValueForEmptyElement);
}

/**
Expand Down Expand Up @@ -281,6 +293,20 @@ public void setXMLTextElementName(String name) {
public String getXMLTextElementName() {
return _cfgNameForTextElement;
}

/**
* @since 2.17
Croway marked this conversation as resolved.
Show resolved Hide resolved
*/
public void setEmptyElementValue(String value) {
_cfgValueForEmptyElement = value;
}

/**
* @since 2.17
*/
public String getEmptyElementValue() {
return _cfgValueForEmptyElement;
}

/*
/**********************************************************
Expand Down Expand Up @@ -560,7 +586,7 @@ public FromXmlParser createParser(XMLStreamReader sr) throws IOException

// false -> not managed
FromXmlParser xp = new FromXmlParser(_createContext(_createContentReference(sr), false),
_parserFeatures, _xmlParserFeatures, _objectCodec, sr, _nameProcessor);
_parserFeatures, _xmlParserFeatures, _objectCodec, sr, _nameProcessor, _cfgValueForEmptyElement);
if (_cfgNameForTextElement != null) {
xp.setXMLTextElementName(_cfgNameForTextElement);
}
Expand Down Expand Up @@ -599,7 +625,7 @@ protected FromXmlParser _createParser(InputStream in, IOContext ctxt) throws IOE
}
sr = _initializeXmlReader(sr);
FromXmlParser xp = new FromXmlParser(ctxt, _parserFeatures, _xmlParserFeatures,
_objectCodec, sr, _nameProcessor);
_objectCodec, sr, _nameProcessor, _cfgValueForEmptyElement);
if (_cfgNameForTextElement != null) {
xp.setXMLTextElementName(_cfgNameForTextElement);
}
Expand All @@ -617,7 +643,7 @@ protected FromXmlParser _createParser(Reader r, IOContext ctxt) throws IOExcepti
}
sr = _initializeXmlReader(sr);
FromXmlParser xp = new FromXmlParser(ctxt, _parserFeatures, _xmlParserFeatures,
_objectCodec, sr, _nameProcessor);
_objectCodec, sr, _nameProcessor, _cfgValueForEmptyElement);
if (_cfgNameForTextElement != null) {
xp.setXMLTextElementName(_cfgNameForTextElement);
}
Expand All @@ -644,7 +670,7 @@ protected FromXmlParser _createParser(char[] data, int offset, int len, IOContex
}
sr = _initializeXmlReader(sr);
FromXmlParser xp = new FromXmlParser(ctxt, _parserFeatures, _xmlParserFeatures,
_objectCodec, sr, _nameProcessor);
_objectCodec, sr, _nameProcessor, _cfgValueForEmptyElement);
if (_cfgNameForTextElement != null) {
xp.setXMLTextElementName(_cfgNameForTextElement);
}
Expand Down Expand Up @@ -678,7 +704,7 @@ protected FromXmlParser _createParser(byte[] data, int offset, int len, IOContex
}
sr = _initializeXmlReader(sr);
FromXmlParser xp = new FromXmlParser(ctxt, _parserFeatures, _xmlParserFeatures,
_objectCodec, sr, _nameProcessor);
_objectCodec, sr, _nameProcessor, _cfgValueForEmptyElement);
if (_cfgNameForTextElement != null) {
xp.setXMLTextElementName(_cfgNameForTextElement);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public class XmlFactoryBuilder extends TSFBuilder<XmlFactory, XmlFactoryBuilder>
*/
protected String _nameForTextElement;

/**
* Set a default value in case of empty an empty element (empty XML tag)
*<p>
* Value used for pseudo-property used for returning empty XML tag.
* Defaults to empty String, but may be changed.
*/
protected String _valueForEmptyElement = FromXmlParser.DEFAULT_EMPTY_ELEMENT_VALUE;

/**
* Optional {@link ClassLoader} to use for constructing
* {@link XMLInputFactory} and {@kink XMLOutputFactory} instances if
Expand Down Expand Up @@ -91,6 +99,7 @@ public XmlFactoryBuilder(XmlFactory base) {
_xmlInputFactory = base._xmlInputFactory;
_xmlOutputFactory = base._xmlOutputFactory;
_nameForTextElement = base._cfgNameForTextElement;
_valueForEmptyElement = base._cfgValueForEmptyElement;
_nameProcessor = base._nameProcessor;
_classLoaderForStax = null;
}
Expand All @@ -102,6 +111,8 @@ public XmlFactoryBuilder(XmlFactory base) {

public String nameForTextElement() { return _nameForTextElement; }

public String valueForEmptyElement() { return _valueForEmptyElement; }

public XMLInputFactory xmlInputFactory() {
if (_xmlInputFactory == null) {
return defaultInputFactory();
Expand Down Expand Up @@ -213,6 +224,11 @@ public XmlFactoryBuilder nameForTextElement(String name) {
return _this();
}

public XmlFactoryBuilder valueForEmptyElement(String value) {
_valueForEmptyElement = value;
return _this();
}

/**
* @since 2.13 (was misnamed as {@code inputFactory(in) formerly})
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ public Builder nameForTextElement(String name) {
return this;
}

public Builder valueForEmptyElement(String value) {
Croway marked this conversation as resolved.
Show resolved Hide resolved
_mapper.setValueForEmptyElement(value);
return this;
}

public Builder defaultUseWrapper(boolean state) {
_mapper.setDefaultUseWrapper(state);
return this;
Expand Down Expand Up @@ -271,6 +276,10 @@ protected void setXMLTextElementName(String name) {
getFactory().setXMLTextElementName(name);
}

protected void setValueForEmptyElement(String value) {
getFactory().setEmptyElementValue(value);
}

/**
* Since 2.7
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class FromXmlParser
*/
public final static String DEFAULT_UNNAMED_TEXT_PROPERTY = "";

public final static String DEFAULT_EMPTY_ELEMENT_VALUE = "";
Croway marked this conversation as resolved.
Show resolved Hide resolved

/**
* XML format has some peculiarities, indicated via new (2.12) capability
* system.
Expand Down Expand Up @@ -159,6 +161,8 @@ private Feature(boolean defaultState) {
*/
protected String _cfgNameForTextElement = DEFAULT_UNNAMED_TEXT_PROPERTY;

protected String _cfgValueForEmptyElement = DEFAULT_EMPTY_ELEMENT_VALUE;

/*
/**********************************************************
/* Configuration
Expand Down Expand Up @@ -276,17 +280,25 @@ private Feature(boolean defaultState) {
*/

public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures,
ObjectCodec codec, XMLStreamReader xmlReader, XmlNameProcessor tagProcessor)
ObjectCodec codec, XMLStreamReader xmlReader, XmlNameProcessor tagProcessor)
throws IOException
{
this(ctxt, genericParserFeatures, xmlFeatures, codec, xmlReader, tagProcessor, FromXmlParser.DEFAULT_EMPTY_ELEMENT_VALUE);
}

public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures,
Croway marked this conversation as resolved.
Show resolved Hide resolved
ObjectCodec codec, XMLStreamReader xmlReader, XmlNameProcessor tagProcessor, String valueForEmptyElement)
throws IOException
{
super(genericParserFeatures);
_cfgValueForEmptyElement = valueForEmptyElement;
_formatFeatures = xmlFeatures;
_ioContext = ctxt;
_streamReadConstraints = ctxt.streamReadConstraints();
_objectCodec = codec;
_parsingContext = XmlReadContext.createRootContext(-1, -1);
_xmlTokens = new XmlTokenStream(xmlReader, ctxt.contentReference(),
_formatFeatures, tagProcessor);
_formatFeatures, _cfgValueForEmptyElement, tagProcessor);

final int firstToken;
try {
Expand Down Expand Up @@ -345,6 +357,13 @@ public void setXMLTextElementName(String name) {
_cfgNameForTextElement = name;
}

/**
* @since 2.17
*/
public void setEmptyElementValue(String value) {
_cfgValueForEmptyElement = value;
}

/*
/**********************************************************************
/* Overrides: capability introspection methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ public class XmlTokenStream
*/
protected String _textValue;

protected String _valueForEmptyElement;
Croway marked this conversation as resolved.
Show resolved Hide resolved

/**
* Marker flag set if caller wants to "push back" current token so
* that next call to {@link #next()} should simply be given what was
Expand Down Expand Up @@ -168,6 +170,12 @@ public class XmlTokenStream

public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef,
int formatFeatures, XmlNameProcessor nameProcessor)
{
this(xmlReader, sourceRef, formatFeatures, FromXmlParser.DEFAULT_EMPTY_ELEMENT_VALUE, nameProcessor);
}

public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef,
int formatFeatures, String valueForEmptyElement, XmlNameProcessor nameProcessor)
{
_sourceReference = sourceRef;
_formatFeatures = formatFeatures;
Expand All @@ -176,6 +184,7 @@ public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef,
// 04-Dec-2023, tatu: [dataformat-xml#618] Need further customized adapter:
_xmlReader = Stax2JacksonReaderAdapter.wrapIfNecessary(xmlReader);
_nameProcessor = nameProcessor;
_valueForEmptyElement = valueForEmptyElement;
}

/**
Expand Down Expand Up @@ -549,6 +558,7 @@ private final int _next() throws XMLStreamException

/**
* @return Collected text, if any, EXCEPT that if {@code FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL}
* OR {@code FromXmlParser.Feature.EMPTY_ELEMENT_AS_EMPTY_ARRAY}
Croway marked this conversation as resolved.
Show resolved Hide resolved
* AND empty element, returns {@code null}
*/
private final String _collectUntilTag() throws XMLStreamException
Expand All @@ -559,7 +569,7 @@ private final String _collectUntilTag() throws XMLStreamException
if (FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL.enabledIn(_formatFeatures)) {
return null;
}
return "";
return _valueForEmptyElement;
}

CharSequence chars = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,26 @@ public void testEmptyElement() throws Exception
assertEquals("", name.last);
}

public void testEmptyElementEmptyArray() throws Exception
{
final String XML = "<name><first/><last></last></name>";

// Default settings (since 2.12): empty element does NOT become `null`:
Name name = MAPPER.readValue(XML, Name.class);
assertNotNull(name);
assertEquals("", name.first);
assertEquals("", name.last);

// but can be changed
XmlMapper mapper2 = XmlMapper.builder()
.valueForEmptyElement("[]")
.build();
name = mapper2.readValue(XML, Name.class);
assertNotNull(name);
assertEquals("[]", name.first);
assertEquals("", name.last);
}

public void testEmptyStringElement() throws Exception
{
// then with empty element
Expand Down