diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactory.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactory.java index 521c9bde..9749698b 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactory.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactory.java @@ -66,6 +66,11 @@ public class XmlFactory extends JsonFactory protected String _cfgNameForTextElement; + /** + * @since 2.17 + */ + protected String _cfgValueForEmptyElement; + protected XmlNameProcessor _nameProcessor; /* @@ -107,18 +112,29 @@ 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()); + } + + /** + * @since 2.17 + */ + 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 @@ -145,6 +161,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; @@ -161,6 +178,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(); @@ -236,8 +254,8 @@ protected Object readResolve() { } catch (Exception e) { throw new IllegalArgumentException(e); } - return new XmlFactory(_objectCodec, _xmlParserFeatures, _xmlGeneratorFeatures, - inf, outf, _cfgNameForTextElement); + return new XmlFactory(_objectCodec, _xmlParserFeatures, _xmlGeneratorFeatures, + inf, outf, _cfgNameForTextElement, _cfgValueForEmptyElement); } /** @@ -281,6 +299,20 @@ public void setXMLTextElementName(String name) { public String getXMLTextElementName() { return _cfgNameForTextElement; } + + /** + * @since 2.17 + */ + public void setEmptyElementValue(String value) { + _cfgValueForEmptyElement = value; + } + + /** + * @since 2.17 + */ + public String getEmptyElementValue() { + return _cfgValueForEmptyElement; + } /* /********************************************************** @@ -560,7 +592,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); } @@ -599,7 +631,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); } @@ -617,7 +649,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); } @@ -644,7 +676,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); } @@ -678,7 +710,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); } diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactoryBuilder.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactoryBuilder.java index 117bbb90..70c2f2d2 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactoryBuilder.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlFactoryBuilder.java @@ -54,6 +54,14 @@ public class XmlFactoryBuilder extends TSFBuilder */ protected String _nameForTextElement; + /** + * Set a default value in case of an empty element (empty XML tag) + *

+ * 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 @@ -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; } @@ -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(); @@ -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}) */ diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlMapper.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlMapper.java index 0fc5c1e9..60755dea 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlMapper.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/XmlMapper.java @@ -104,6 +104,20 @@ public Builder nameForTextElement(String name) { return this; } + /** + * + * Set a default value in case of an empty element (empty XML tag) + *

+ * In case of an empty XML tag (like ``) the serialized value + * is set to `String value`. If not specified, the default value is empty String. + * + * @since 2.17 + */ + public Builder valueForEmptyElement(String value) { + _mapper.setValueForEmptyElement(value); + return this; + } + public Builder defaultUseWrapper(boolean state) { _mapper.setDefaultUseWrapper(state); return this; @@ -271,6 +285,15 @@ protected void setXMLTextElementName(String name) { getFactory().setXMLTextElementName(name); } + // Needed by Builder itself in 2.x, but should not be called by users hence: + /** + * @deprecated Since 2.17 use {@link Builder#valueForEmptyElement(String)} instead + */ + @Deprecated + protected void setValueForEmptyElement(String value) { + getFactory().setEmptyElementValue(value); + } + /** * Since 2.7 * diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java index 654aafbe..cd555b2b 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java @@ -41,6 +41,12 @@ public class FromXmlParser */ public final static String DEFAULT_UNNAMED_TEXT_PROPERTY = ""; + /** + * The default value placeholder for XML empty tag is an empty + * String (""). + */ + public final static String DEFAULT_EMPTY_ELEMENT_VALUE = ""; + /** * XML format has some peculiarities, indicated via new (2.12) capability * system. @@ -160,6 +166,15 @@ private Feature(boolean defaultState) { */ protected String _cfgNameForTextElement = DEFAULT_UNNAMED_TEXT_PROPERTY; + /** + * When an empty element (like {@code }) is encountered, this + * textual value reported in the token stream: default is empty + * String, but may be configured for any other String value. + * + * @since 2.17 + */ + protected final String _cfgValueForEmptyElement; + /* /********************************************************** /* Configuration @@ -276,18 +291,35 @@ private Feature(boolean defaultState) { /********************************************************** */ + /** + * @deprecated Since 2.17 + */ + @Deprecated + public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures, + ObjectCodec codec, XMLStreamReader xmlReader, XmlNameProcessor tagProcessor) + throws IOException + { + this(ctxt, genericParserFeatures, xmlFeatures, codec, xmlReader, tagProcessor, + FromXmlParser.DEFAULT_EMPTY_ELEMENT_VALUE); + } + + /** + * @since 2.17 + */ public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures, - ObjectCodec codec, XMLStreamReader xmlReader, XmlNameProcessor tagProcessor) + 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 { diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java index 8e132016..b45f5229 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java @@ -120,6 +120,8 @@ public class XmlTokenStream */ protected String _textValue; + protected final String _valueForEmptyElement; + /** * Marker flag set if caller wants to "push back" current token so * that next call to {@link #next()} should simply be given what was @@ -166,8 +168,19 @@ public class XmlTokenStream /********************************************************************** */ + @Deprecated // since 2.17 public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef, int formatFeatures, XmlNameProcessor nameProcessor) + { + this(xmlReader, sourceRef, formatFeatures, FromXmlParser.DEFAULT_EMPTY_ELEMENT_VALUE, + nameProcessor); + } + + /** + * @since 2.17 + */ + public XmlTokenStream(XMLStreamReader xmlReader, ContentReference sourceRef, + int formatFeatures, String valueForEmptyElement, XmlNameProcessor nameProcessor) { _sourceReference = sourceRef; _formatFeatures = formatFeatures; @@ -176,6 +189,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; } /** @@ -559,7 +573,7 @@ private final String _collectUntilTag() throws XMLStreamException if (FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL.enabledIn(_formatFeatures)) { return null; } - return ""; + return _valueForEmptyElement; } CharSequence chars = null; diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/EmptyStringValueTest.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/EmptyStringValueTest.java index 79f72971..2f0865a5 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/EmptyStringValueTest.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/EmptyStringValueTest.java @@ -85,6 +85,26 @@ public void testEmptyElement() throws Exception assertEquals("", name.last); } + public void testEmptyElementEmptyArray() throws Exception + { + final String XML = ""; + + // 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 diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/stream/XmlTokenStreamTest.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/stream/XmlTokenStreamTest.java index 2936ef06..51d2aafd 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/stream/XmlTokenStreamTest.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/stream/XmlTokenStreamTest.java @@ -181,7 +181,8 @@ private XmlTokenStream _tokensFor(String doc, int flags) throws Exception XMLStreamReader sr = XML_FACTORY.getXMLInputFactory().createXMLStreamReader(new StringReader(doc)); // must point to START_ELEMENT, so: sr.nextTag(); - XmlTokenStream stream = new XmlTokenStream(sr, ContentReference.rawReference(doc), flags, XmlNameProcessors.newPassthroughProcessor()); + XmlTokenStream stream = new XmlTokenStream(sr, ContentReference.rawReference(doc), flags, + "", XmlNameProcessors.newPassthroughProcessor()); stream.initialize(); return stream; }