Skip to content

Commit e1d1484

Browse files
committed
Implement #849: allow adding comments in doc prolog
1 parent f576a8b commit e1d1484

7 files changed

Lines changed: 114 additions & 34 deletions

File tree

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ Version: 3.x (for earlier see VERSION-2.x)
119119
(fix by @cowtowncoder, w/ Claude code)
120120
#845: Implement `JsonGenerator` methods `writeComment()` and `canWriteComments()`
121121
(implemented by @cowtowncoder, w/ Claude code)
122+
#849: Allow writing Comments in Document Prolog (before root element)
123+
(implemented by @cowtowncoder)
122124

123125
3.1.2 (11-Apr-2026)
124126
3.1.1 (27-Mar-2026)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package tools.jackson.dataformat.xml.ser;
2+
3+
import javax.xml.stream.XMLStreamException;
4+
5+
import org.codehaus.stax2.XMLStreamWriter2;
6+
7+
import tools.jackson.dataformat.xml.util.ArgUtil;
8+
9+
/**
10+
* Value container to represent XML Comment within "prolog"
11+
* part of the Document (before XML Root element, after XML
12+
* declaration if one written),
13+
* to be written using {@link XmlGeneratorInitializer}.
14+
*
15+
* @since 3.2
16+
*/
17+
public record Comment(String content)
18+
implements XmlPrologDirective
19+
{
20+
public Comment {
21+
content = ArgUtil.nullToEmpty(content);
22+
}
23+
24+
@Override
25+
public void write(ToXmlGenerator xmlGen, XMLStreamWriter2 sw) throws XMLStreamException {
26+
sw.writeComment(content);
27+
}
28+
29+
}

src/main/java/tools/jackson/dataformat/xml/ser/DTD.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import org.codehaus.stax2.XMLStreamWriter2;
66

7+
import tools.jackson.dataformat.xml.util.ArgUtil;
8+
79
/**
810
* Value container to represent XML Document Type Declaration,
911
* to be written using {@link XmlGeneratorInitializer}.
@@ -16,22 +18,10 @@ public record DTD(String rootName,
1618
implements XmlPrologDirective
1719
{
1820
public DTD {
19-
rootName = _nonEmptyNonNull("rootName", rootName);
20-
systemId = _emptyToNull(systemId);
21-
publicId = _emptyToNull(publicId);
22-
internalSubset = _emptyToNull(internalSubset);
23-
}
24-
25-
static String _emptyToNull(String str) {
26-
return "".equals(str) ? null : str;
27-
}
28-
29-
static String _nonEmptyNonNull(String prop, String str) {
30-
if (str == null || str.isEmpty()) {
31-
throw new IllegalArgumentException("Illegal argument for '%s': must be non-empty String"
32-
.formatted(prop));
33-
}
34-
return str;
21+
rootName = ArgUtil.nonEmptyNonNull("rootName", rootName);
22+
systemId = ArgUtil.emptyToNull(systemId);
23+
publicId = ArgUtil.emptyToNull(publicId);
24+
internalSubset = ArgUtil.emptyToNull(internalSubset);
3525
}
3626

3727
@Override

src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,8 @@ public void initGenerator() throws JacksonException
249249
}
250250

251251
// as per [dataformat-xml#172], try adding indentation
252-
if (xmlDeclWritten && (_xmlPrettyPrinter != null)) {
253-
// ... but only if it is likely to succeed:
254-
if (!_stax2Emulation) {
255-
_xmlPrettyPrinter.writePrologLinefeed(_xmlWriter);
256-
}
252+
if (xmlDeclWritten) {
253+
_prologLinefeed();
257254
}
258255
if (XmlWriteFeature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)) {
259256
_xmlWriter.setPrefix("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
@@ -263,6 +260,8 @@ public void initGenerator() throws JacksonException
263260
if (_prologDirectives != null) {
264261
for (XmlPrologDirective d : _prologDirectives) {
265262
d.write(this, _xmlWriter);
263+
// Add linefeed separators b/w directives
264+
_prologLinefeed();
266265
}
267266
}
268267

@@ -271,6 +270,18 @@ public void initGenerator() throws JacksonException
271270
}
272271
}
273272

273+
// @since 3.2
274+
private void _prologLinefeed() throws XMLStreamException
275+
{
276+
if (!_stax2Emulation) {
277+
if (_xmlPrettyPrinter != null) {
278+
_xmlPrettyPrinter.writePrologLinefeed(_xmlWriter);
279+
} else {
280+
_xmlWriter.writeSpace("\n");
281+
}
282+
}
283+
}
284+
274285
/**
275286
* Method called by {@link XmlGeneratorInitializer} to inject
276287
* necessary configuration.

src/main/java/tools/jackson/dataformat/xml/ser/XmlGeneratorInitializer.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ public void initialize(SerializationConfig config, JsonGenerator g) throws Jacks
3838
}
3939
}
4040

41+
/**
42+
* Method for adding XML comment; to be written in position added
43+
* with respective to other directives
44+
* (but always after XML Declaration which most come before any other output;
45+
* and before Document Root element)
46+
*
47+
* @param commentContent (option) Comment content to include
48+
*
49+
* @return This initializer for call chaining
50+
*/
51+
public XmlGeneratorInitializer addComment(String commentContent) {
52+
return _add(new Comment(commentContent));
53+
}
54+
4155
/**
4256
* Convenience method that constructs {@link DTD} out of arguments
4357
* and calls {@link #addDTD(DTD)}.
@@ -57,10 +71,10 @@ public XmlGeneratorInitializer addDTD(String rootName,
5771
}
5872

5973
/**
60-
* Method for adding Document Type Declaration (DTD) directive; to write
61-
* in order added with respective to other directives (but always after
62-
* XML Declaration which most come before any other output; and before
63-
* Document Root element)
74+
* Method for adding Document Type Declaration (DTD) directive; to
75+
* be written in position added with respective to other directives
76+
* (but always after XML Declaration which most come before any other output;
77+
* and before Document Root element)
6478
*
6579
* @param dtd DTD to write
6680
*
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package tools.jackson.dataformat.xml.util;
2+
3+
public abstract class ArgUtil
4+
{
5+
public static String emptyToNull(String str) {
6+
return "".equals(str) ? null : str;
7+
}
8+
9+
public static String nullToEmpty(String str) {
10+
return (str == null) ? "" : str;
11+
}
12+
13+
public static String nonEmptyNonNull(String prop, String str) {
14+
if (str == null || str.isEmpty()) {
15+
throw new IllegalArgumentException("Illegal argument for '%s': must be non-empty String"
16+
.formatted(prop));
17+
}
18+
return str;
19+
}
20+
}

src/test/java/tools/jackson/dataformat/xml/ser/XmlGeneratorInitializerTest.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ public class XmlGeneratorInitializerTest extends XmlTestUtil
1414
{
1515
private final XmlMapper MAPPER = newMapper();
1616

17-
// [dataformat-xml#150]: DTD writing -- ok cases
17+
// // [dataformat-xml#150]: DTD writing -- ok cases
18+
1819
@Test
1920
public void testDTDWithOnlyRootElement() throws Exception
2021
{
2122
ObjectWriter w = MAPPER.writer().with(
2223
new XmlGeneratorInitializer()
2324
.addDTD("StringBean", null, null, null));
24-
assertEquals(a2q("<!DOCTYPE StringBean>"
25+
assertEquals(a2q("<!DOCTYPE StringBean>\n"
2526
+"<StringBean><text>test</text></StringBean>"),
2627
w.writeValueAsString(new StringBean("test")));
2728
}
@@ -32,7 +33,7 @@ public void testDTDWithPublicId() throws Exception
3233
ObjectWriter w = MAPPER.writer().with(
3334
new XmlGeneratorInitializer()
3435
.addDTD("StringBean", "system", "http://foo.bar", ""));
35-
assertEquals(a2q("<!DOCTYPE StringBean PUBLIC 'http://foo.bar' 'system'>"
36+
assertEquals(a2q("<!DOCTYPE StringBean PUBLIC 'http://foo.bar' 'system'>\n"
3637
+"<StringBean><text>test</text></StringBean>"),
3738
w.writeValueAsString(new StringBean("test")));
3839
}
@@ -43,7 +44,7 @@ public void testDTDWithSystemIdOnly() throws Exception
4344
ObjectWriter w = MAPPER.writer().with(
4445
new XmlGeneratorInitializer()
4546
.addDTD("StringBean", "system", "", null));
46-
assertEquals(a2q("<!DOCTYPE StringBean SYSTEM 'system'>"
47+
assertEquals(a2q("<!DOCTYPE StringBean SYSTEM 'system'>\n"
4748
+"<StringBean><text>test</text></StringBean>"),
4849
w.writeValueAsString(new StringBean("test")));
4950
}
@@ -55,7 +56,7 @@ public void testDTDWithInternalSubset() throws Exception
5556
new XmlGeneratorInitializer()
5657
.addDTD("StringBean", "system", "http://foo.bar", "<!ELEMENT root (#PCDATA)>"));
5758
assertEquals(a2q("<!DOCTYPE StringBean PUBLIC 'http://foo.bar' 'system' "
58-
+"[<!ELEMENT root (#PCDATA)>]>"
59+
+"[<!ELEMENT root (#PCDATA)>]>\n"
5960
+"<StringBean><text>test</text></StringBean>"),
6061
w.writeValueAsString(new StringBean("test")));
6162
}
@@ -72,13 +73,13 @@ public void testDTDWithXmlDeclaration() throws Exception
7273
.addDTD("StringBean", "system", "http://foo.bar", null));
7374
// XML declaration is emitted with single quotes, DOCTYPE with double quotes,
7475
// so cannot use a2q() on the whole string here.
75-
assertEquals("<?xml version='1.0' encoding='UTF-8'?>"
76-
+"<!DOCTYPE StringBean PUBLIC \"http://foo.bar\" \"system\">"
76+
assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n"
77+
+"<!DOCTYPE StringBean PUBLIC \"http://foo.bar\" \"system\">\n"
7778
+"<StringBean><text>test</text></StringBean>",
7879
w.writeValueAsString(new StringBean("test")));
7980
}
8081

81-
// [dataformat-xml#150]: DTD writing -- failing cases
82+
// // [dataformat-xml#150]: DTD writing -- failing cases
8283
@Test
8384
public void testDTDInvalidNoRoot() throws Exception
8485
{
@@ -92,5 +93,18 @@ public void testDTDInvalidNoRoot() throws Exception
9293
}
9394
}
9495

95-
// Other tests
96+
// // [dataformat-xml#849]: Comment writing -- ok cases
97+
98+
@Test
99+
public void testSimpleComment() throws Exception
100+
{
101+
ObjectWriter w = MAPPER.writer().with(
102+
new XmlGeneratorInitializer()
103+
.addComment("Comment content!"));
104+
assertEquals(a2q("<!--Comment content!-->\n"
105+
+"<StringBean><text>test</text></StringBean>"),
106+
w.writeValueAsString(new StringBean("test")));
107+
}
108+
109+
// // Other tests
96110
}

0 commit comments

Comments
 (0)