Skip to content

Commit d5cd57f

Browse files
authored
Implement #849: allow adding comments in doc prolog (#851)
1 parent f576a8b commit d5cd57f

7 files changed

Lines changed: 265 additions & 56 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
}

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: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ public class ToXmlGenerator
106106
*/
107107
protected List<XmlPrologDirective> _prologDirectives;
108108

109+
/**
110+
* Whether linefeed ("pretty-printing") enabled between directives
111+
* in Document prolog.
112+
*
113+
* @since 3.2
114+
*/
115+
protected boolean _lfBetweenPrologDirectives;
116+
109117
/*
110118
/**********************************************************************
111119
/* Logical output state
@@ -218,6 +226,22 @@ public ToXmlGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
218226
/**********************************************************************
219227
*/
220228

229+
/**
230+
* Method called by {@link XmlGeneratorInitializer} to inject
231+
* necessary configuration.
232+
*
233+
* @since 3.2
234+
*/
235+
public void initProlog(boolean lfBetweenPrologDirectives,
236+
List<XmlPrologDirective> directives)
237+
{
238+
if (_initialized) { // sanity check
239+
_reportError("Internal error: cannot call `initConfig()` after generator already initialized");
240+
}
241+
_lfBetweenPrologDirectives = lfBetweenPrologDirectives;
242+
_prologDirectives = directives;
243+
}
244+
221245
/**
222246
* Method called by {@link XmlSerializationContext} before writing any output,
223247
* to optionally output XML declaration and other before-root-element
@@ -230,29 +254,21 @@ public void initGenerator() throws JacksonException
230254
}
231255
_initialized = true;
232256
try {
233-
boolean xmlDeclWritten;
234-
235-
if (XmlWriteFeature.WRITE_XML_1_1.enabledIn(_formatFeatures)
236-
|| XmlWriteFeature.WRITE_XML_DECLARATION.enabledIn(_formatFeatures)) {
257+
final boolean xml11Decl = XmlWriteFeature.WRITE_XML_1_1.enabledIn(_formatFeatures);
258+
if (xml11Decl || XmlWriteFeature.WRITE_XML_DECLARATION.enabledIn(_formatFeatures)) {
237259

238-
String xmlVersion = XmlWriteFeature.WRITE_XML_1_1.enabledIn(_formatFeatures) ? "1.1" : "1.0";
260+
String xmlVersion = xml11Decl ? "1.1" : "1.0";
239261
String encoding = "UTF-8";
240262

241263
if (XmlWriteFeature.WRITE_STANDALONE_YES_TO_XML_DECLARATION.enabledIn(_formatFeatures)) {
242264
_xmlWriter.writeStartDocument(xmlVersion, encoding, true);
243265
} else {
244266
_xmlWriter.writeStartDocument(encoding, xmlVersion);
245267
}
246-
xmlDeclWritten = true;
247-
} else {
248-
xmlDeclWritten = false;
249-
}
250-
251-
// 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);
268+
// 20-Apr-2026, tatu: for legacy path, only output prolog lf when pretty-printing
269+
// OR _lfBetweenPrologDirectives passed by initializer
270+
if (_lfBetweenPrologDirectives || _xmlPrettyPrinter != null) {
271+
_prologLinefeed();
256272
}
257273
}
258274
if (XmlWriteFeature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)) {
@@ -263,6 +279,10 @@ public void initGenerator() throws JacksonException
263279
if (_prologDirectives != null) {
264280
for (XmlPrologDirective d : _prologDirectives) {
265281
d.write(this, _xmlWriter);
282+
// Add linefeed separators b/w directives
283+
if (_lfBetweenPrologDirectives) {
284+
_prologLinefeed();
285+
}
266286
}
267287
}
268288

@@ -271,18 +291,16 @@ public void initGenerator() throws JacksonException
271291
}
272292
}
273293

274-
/**
275-
* Method called by {@link XmlGeneratorInitializer} to inject
276-
* necessary configuration.
277-
*
278-
* @since 3.2
279-
*/
280-
public void initProlog(List<XmlPrologDirective> directives)
294+
// @since 3.2
295+
private void _prologLinefeed() throws XMLStreamException
281296
{
282-
if (_initialized) { // sanity check
283-
_reportError("Internal error: cannot call `initConfig()` after generator already initialized");
297+
if (!_stax2Emulation) {
298+
if (_xmlPrettyPrinter != null) {
299+
_xmlPrettyPrinter.writePrologLinefeed(_xmlWriter);
300+
} else {
301+
_xmlWriter.writeSpace("\n");
302+
}
284303
}
285-
_prologDirectives = directives;
286304
}
287305

288306
/*

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
* {@link ObjectWriter#with(GeneratorInitializer)}.
1717
* It allows output of various document-level things such as
1818
*<ul>
19-
* <li>Document Type Declarations (DTD); that is "&lt;!DOCTYPE>" directive
19+
* <li>Document Type Declarations (DTD); that is "&lt;!DOCTYPE>" directive
20+
* </li>
21+
* <li>XML Comments (in Document prolog, before the root element)
2022
* </li>
2123
* </ul>
2224
*<p>
@@ -29,15 +31,44 @@ public class XmlGeneratorInitializer
2931
{
3032
protected List<XmlPrologDirective> _directives;
3133

34+
protected boolean _addLfBetweenPrologDirectives = true;
35+
3236
protected boolean _hasDTD;
3337

3438
@Override
3539
public void initialize(SerializationConfig config, JsonGenerator g) throws JacksonException {
3640
if (g instanceof ToXmlGenerator xg) {
37-
xg.initProlog(_directives);
41+
xg.initProlog(_addLfBetweenPrologDirectives, _directives);
3842
}
3943
}
4044

45+
/**
46+
* Method to change whether line-feeds are to be added between Prolog directives
47+
* or not: default being they are (enabled).
48+
*
49+
* @param addLFs Whether line-feeds are to be added or not (default: {@code true})
50+
*
51+
* @return This initializer for call chaining
52+
*/
53+
public XmlGeneratorInitializer linefeedsBetweenPrologDirectives(boolean addLFs) {
54+
_addLfBetweenPrologDirectives = addLFs;
55+
return this;
56+
}
57+
58+
/**
59+
* Method for adding XML comment; to be written in position added
60+
* with respective to other directives
61+
* (but always after XML Declaration which must come before any other output;
62+
* and before Document Root element)
63+
*
64+
* @param commentContent (optional) Comment content to include
65+
*
66+
* @return This initializer for call chaining
67+
*/
68+
public XmlGeneratorInitializer addComment(String commentContent) {
69+
return _add(new Comment(commentContent));
70+
}
71+
4172
/**
4273
* Convenience method that constructs {@link DTD} out of arguments
4374
* and calls {@link #addDTD(DTD)}.
@@ -57,10 +88,10 @@ public XmlGeneratorInitializer addDTD(String rootName,
5788
}
5889

5990
/**
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)
91+
* Method for adding Document Type Declaration (DTD) directive; to
92+
* be written in position added with respective to other directives
93+
* (but always after XML Declaration which must come before any other output;
94+
* and before Document Root element)
6495
*
6596
* @param dtd DTD to write
6697
*
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+
}

0 commit comments

Comments
 (0)