diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java
index b5963d38..774bf2b9 100644
--- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java
+++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java
@@ -644,6 +644,9 @@ private final String _collectUntilTag() throws XMLStreamException
// note: SPACE is ignorable (and seldom seen), not to be included
case XMLStreamConstants.CHARACTERS:
case XMLStreamConstants.CDATA:
+ // Only reported if the reader does not replace entity references
+ // (or could not expand this one): still part of text content
+ case XMLStreamConstants.ENTITY_REFERENCE:
// 17-Jul-2017, tatu: as per [dataformat-xml#236], need to try to...
{
String str = _getText(_xmlReader);
@@ -684,6 +687,7 @@ private final int _skipAndCollectTextUntilTag() throws XMLStreamException
// note: SPACE is ignorable (and seldom seen), not to be included
case XMLStreamConstants.CHARACTERS:
case XMLStreamConstants.CDATA:
+ case XMLStreamConstants.ENTITY_REFERENCE:
{
String str = _getText(_xmlReader);
if (chars == null) {
@@ -705,8 +709,9 @@ private final int _skipAndCollectTextUntilTag() throws XMLStreamException
private final String _getText(XMLStreamReader2 r) throws XMLStreamException
{
+ final String text;
try {
- return r.getText();
+ text = r.getText();
} catch (RuntimeException e) {
Throwable cause = e.getCause();
if (cause instanceof XMLStreamException xse) {
@@ -714,6 +719,14 @@ private final String _getText(XMLStreamReader2 r) throws XMLStreamException
}
throw e;
}
+ // An entity reference the reader did not (or could not) expand has no
+ // replacement text to offer: fail rather than silently drop it from content
+ if (text == null && r.getEventType() == XMLStreamConstants.ENTITY_REFERENCE) {
+ throw new XMLStreamException("Unexpanded entity reference '&"+r.getLocalName()
+ +";' in text content (entity not declared, or not replaced by XMLStreamReader)",
+ r.getLocation());
+ }
+ return text;
}
/*
diff --git a/src/test/java/tools/jackson/dataformat/xml/stream/EntityReferenceReadTest.java b/src/test/java/tools/jackson/dataformat/xml/stream/EntityReferenceReadTest.java
new file mode 100644
index 00000000..cab33124
--- /dev/null
+++ b/src/test/java/tools/jackson/dataformat/xml/stream/EntityReferenceReadTest.java
@@ -0,0 +1,90 @@
+package tools.jackson.dataformat.xml.stream;
+
+import java.util.Map;
+
+import javax.xml.stream.XMLInputFactory;
+
+import org.junit.jupiter.api.Test;
+
+import com.ctc.wstx.stax.WstxInputFactory;
+
+import tools.jackson.core.exc.StreamReadException;
+
+import tools.jackson.dataformat.xml.XmlFactory;
+import tools.jackson.dataformat.xml.XmlMapper;
+import tools.jackson.dataformat.xml.XmlTestUtil;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+// Readers configured with `IS_REPLACING_ENTITY_REFERENCES` disabled report
+// general entity references in text content as `ENTITY_REFERENCE` events.
+// `XmlTokenStream` used to ignore those while collecting text, silently
+// dropping part of the content: either the replacement text has to be used,
+// or (if the reader has none to offer) reading must fail.
+public class EntityReferenceReadTest extends XmlTestUtil
+{
+ private final static String DOC_INTERNAL_ENTITY =
+ "]>\n"
+ +"foo&e;bar";
+
+ private final static String DOC_UNDECLARED_ENTITY =
+ "foo&e;bar";
+
+ // Text collected after an END_ELEMENT goes through a different code path
+ // (mixed content), so cover that one too
+ private final static String DOC_MIXED_CONTENT =
+ "]>\n"
+ +"1foo&e;bar2";
+
+ private final XmlMapper NON_REPLACING_MAPPER = _nonReplacingMapper(true);
+
+ private final XmlMapper NON_REPLACING_NO_DTD_MAPPER = _nonReplacingMapper(false);
+
+ private static XmlMapper _nonReplacingMapper(boolean supportDTD) {
+ XMLInputFactory f = new WstxInputFactory();
+ f.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE);
+ f.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+ f.setProperty(XMLInputFactory.SUPPORT_DTD, supportDTD);
+ return mapperBuilder(XmlFactory.builder().xmlInputFactory(f).build()).build();
+ }
+
+ @Test
+ public void testDeclaredEntityNotReplacedByReader() throws Exception
+ {
+ Map,?> result = NON_REPLACING_MAPPER.readValue(DOC_INTERNAL_ENTITY, Map.class);
+ assertEquals("fooxxbar", result.get("a"));
+ }
+
+ @Test
+ public void testDeclaredEntityNotReplacedByReaderMixedContent() throws Exception
+ {
+ Map,?> result = NON_REPLACING_MAPPER.readValue(DOC_MIXED_CONTENT, Map.class);
+ assertEquals("1", result.get("a"));
+ assertEquals("2", result.get("b"));
+ assertEquals("fooxxbar", result.get(""));
+ }
+
+ @Test
+ public void testUndeclaredEntityNotReplacedByReader() throws Exception
+ {
+ // No DTD, so no replacement text for the reader to hand out: must fail,
+ // not quietly produce "foobar"
+ StreamReadException e = assertThrows(StreamReadException.class,
+ () -> NON_REPLACING_NO_DTD_MAPPER.readValue(DOC_UNDECLARED_ENTITY, Map.class));
+ verifyException(e, "Unexpanded entity reference '&e;'");
+ }
+
+ @Test
+ public void testDefaultReaderStillReplaces() throws Exception
+ {
+ // Default (replacing) reader with DTD support enabled expands the entity
+ // itself; no change in behavior there
+ XMLInputFactory f = new WstxInputFactory();
+ f.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+ f.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.TRUE);
+ XmlMapper mapper = mapperBuilder(XmlFactory.builder().xmlInputFactory(f).build()).build();
+ Map,?> result = mapper.readValue(DOC_INTERNAL_ENTITY, Map.class);
+ assertEquals("fooxxbar", result.get("a"));
+ }
+}