Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ Version: 3.x (for earlier see VERSION-2.x)
(fix by @cowtowncoder, w/ Claude code)
#845: Implement `JsonGenerator` methods `writeComment()` and `canWriteComments()`
(implemented by @cowtowncoder, w/ Claude code)
#849: Allow writing Comments in Document Prolog (before root element)
(implemented by @cowtowncoder)

3.1.2 (11-Apr-2026)
3.1.1 (27-Mar-2026)
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/tools/jackson/dataformat/xml/ser/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tools.jackson.dataformat.xml.ser;

import javax.xml.stream.XMLStreamException;

import org.codehaus.stax2.XMLStreamWriter2;

import tools.jackson.dataformat.xml.util.ArgUtil;

/**
* Value container to represent XML Comment within "prolog"
* part of the Document (before XML Root element, after XML
* declaration if one written),
* to be written using {@link XmlGeneratorInitializer}.
*
* @since 3.2
*/
public record Comment(String content)
implements XmlPrologDirective
{
public Comment {
content = ArgUtil.nullToEmpty(content);
}

@Override
public void write(ToXmlGenerator xmlGen, XMLStreamWriter2 sw) throws XMLStreamException {
sw.writeComment(content);
}
}
22 changes: 6 additions & 16 deletions src/main/java/tools/jackson/dataformat/xml/ser/DTD.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import org.codehaus.stax2.XMLStreamWriter2;

import tools.jackson.dataformat.xml.util.ArgUtil;

/**
* Value container to represent XML Document Type Declaration,
* to be written using {@link XmlGeneratorInitializer}.
Expand All @@ -16,22 +18,10 @@ public record DTD(String rootName,
implements XmlPrologDirective
{
public DTD {
rootName = _nonEmptyNonNull("rootName", rootName);
systemId = _emptyToNull(systemId);
publicId = _emptyToNull(publicId);
internalSubset = _emptyToNull(internalSubset);
}

static String _emptyToNull(String str) {
return "".equals(str) ? null : str;
}

static String _nonEmptyNonNull(String prop, String str) {
if (str == null || str.isEmpty()) {
throw new IllegalArgumentException("Illegal argument for '%s': must be non-empty String"
.formatted(prop));
}
return str;
rootName = ArgUtil.nonEmptyNonNull("rootName", rootName);
systemId = ArgUtil.emptyToNull(systemId);
publicId = ArgUtil.emptyToNull(publicId);
internalSubset = ArgUtil.emptyToNull(internalSubset);
}

@Override
Expand Down
68 changes: 43 additions & 25 deletions src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ public class ToXmlGenerator
*/
protected List<XmlPrologDirective> _prologDirectives;

/**
* Whether linefeed ("pretty-printing") enabled between directives
* in Document prolog.
*
* @since 3.2
*/
protected boolean _lfBetweenPrologDirectives;

/*
/**********************************************************************
/* Logical output state
Expand Down Expand Up @@ -218,6 +226,22 @@ public ToXmlGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
/**********************************************************************
*/

/**
* Method called by {@link XmlGeneratorInitializer} to inject
* necessary configuration.
*
* @since 3.2
*/
public void initProlog(boolean lfBetweenPrologDirectives,
List<XmlPrologDirective> directives)
{
if (_initialized) { // sanity check
_reportError("Internal error: cannot call `initConfig()` after generator already initialized");
}
_lfBetweenPrologDirectives = lfBetweenPrologDirectives;
_prologDirectives = directives;
}

/**
* Method called by {@link XmlSerializationContext} before writing any output,
* to optionally output XML declaration and other before-root-element
Expand All @@ -230,29 +254,21 @@ public void initGenerator() throws JacksonException
}
_initialized = true;
try {
boolean xmlDeclWritten;

if (XmlWriteFeature.WRITE_XML_1_1.enabledIn(_formatFeatures)
|| XmlWriteFeature.WRITE_XML_DECLARATION.enabledIn(_formatFeatures)) {
final boolean xml11Decl = XmlWriteFeature.WRITE_XML_1_1.enabledIn(_formatFeatures);
if (xml11Decl || XmlWriteFeature.WRITE_XML_DECLARATION.enabledIn(_formatFeatures)) {

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

if (XmlWriteFeature.WRITE_STANDALONE_YES_TO_XML_DECLARATION.enabledIn(_formatFeatures)) {
_xmlWriter.writeStartDocument(xmlVersion, encoding, true);
} else {
_xmlWriter.writeStartDocument(encoding, xmlVersion);
}
xmlDeclWritten = true;
} else {
xmlDeclWritten = false;
}

// as per [dataformat-xml#172], try adding indentation
if (xmlDeclWritten && (_xmlPrettyPrinter != null)) {
// ... but only if it is likely to succeed:
if (!_stax2Emulation) {
_xmlPrettyPrinter.writePrologLinefeed(_xmlWriter);
// 20-Apr-2026, tatu: for legacy path, only output prolog lf when pretty-printing
// OR _lfBetweenPrologDirectives passed by initializer
if (_lfBetweenPrologDirectives || _xmlPrettyPrinter != null) {
_prologLinefeed();
}
}
if (XmlWriteFeature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)) {
Expand All @@ -263,6 +279,10 @@ public void initGenerator() throws JacksonException
if (_prologDirectives != null) {
for (XmlPrologDirective d : _prologDirectives) {
d.write(this, _xmlWriter);
// Add linefeed separators b/w directives
if (_lfBetweenPrologDirectives) {
_prologLinefeed();
}
}
}

Expand All @@ -271,18 +291,16 @@ public void initGenerator() throws JacksonException
}
}

/**
* Method called by {@link XmlGeneratorInitializer} to inject
* necessary configuration.
*
* @since 3.2
*/
public void initProlog(List<XmlPrologDirective> directives)
// @since 3.2
private void _prologLinefeed() throws XMLStreamException
{
if (_initialized) { // sanity check
_reportError("Internal error: cannot call `initConfig()` after generator already initialized");
if (!_stax2Emulation) {
if (_xmlPrettyPrinter != null) {
_xmlPrettyPrinter.writePrologLinefeed(_xmlWriter);
} else {
_xmlWriter.writeSpace("\n");
}
}
_prologDirectives = directives;
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
* {@link ObjectWriter#with(GeneratorInitializer)}.
* It allows output of various document-level things such as
*<ul>
* <li>Document Type Declarations (DTD); that is "&lt;!DOCTYPE>" directive
* <li>Document Type Declarations (DTD); that is "&lt;!DOCTYPE>" directive
* </li>
* <li>XML Comments (in Document prolog, before the root element)
* </li>
* </ul>
*<p>
Expand All @@ -29,15 +31,44 @@ public class XmlGeneratorInitializer
{
protected List<XmlPrologDirective> _directives;

protected boolean _addLfBetweenPrologDirectives = true;

protected boolean _hasDTD;

@Override
public void initialize(SerializationConfig config, JsonGenerator g) throws JacksonException {
if (g instanceof ToXmlGenerator xg) {
xg.initProlog(_directives);
xg.initProlog(_addLfBetweenPrologDirectives, _directives);
}
}

/**
* Method to change whether line-feeds are to be added between Prolog directives
* or not: default being they are (enabled).
*
* @param addLFs Whether line-feeds are to be added or not (default: {@code true})
*
* @return This initializer for call chaining
*/
public XmlGeneratorInitializer linefeedsBetweenPrologDirectives(boolean addLFs) {
_addLfBetweenPrologDirectives = addLFs;
return this;
}

/**
* Method for adding XML comment; to be written in position added
* with respective to other directives
* (but always after XML Declaration which must come before any other output;
* and before Document Root element)
*
* @param commentContent (optional) Comment content to include
*
* @return This initializer for call chaining
*/
public XmlGeneratorInitializer addComment(String commentContent) {
return _add(new Comment(commentContent));
}

/**
* Convenience method that constructs {@link DTD} out of arguments
* and calls {@link #addDTD(DTD)}.
Expand All @@ -57,10 +88,10 @@ public XmlGeneratorInitializer addDTD(String rootName,
}

/**
* Method for adding Document Type Declaration (DTD) directive; to write
* in order added with respective to other directives (but always after
* XML Declaration which most come before any other output; and before
* Document Root element)
* Method for adding Document Type Declaration (DTD) directive; to
* be written in position added with respective to other directives
* (but always after XML Declaration which must come before any other output;
* and before Document Root element)
*
* @param dtd DTD to write
*
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/tools/jackson/dataformat/xml/util/ArgUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tools.jackson.dataformat.xml.util;

public abstract class ArgUtil
{
public static String emptyToNull(String str) {
return "".equals(str) ? null : str;
}

public static String nullToEmpty(String str) {
return (str == null) ? "" : str;
}

public static String nonEmptyNonNull(String prop, String str) {
if (str == null || str.isEmpty()) {
throw new IllegalArgumentException("Illegal argument for '%s': must be non-empty String"
.formatted(prop));
}
return str;
}
}
Loading