Skip to content

Commit 3077080

Browse files
authored
Implement #90: allow adding attributes to root element (#859)
1 parent b359508 commit 3077080

5 files changed

Lines changed: 319 additions & 4 deletions

File tree

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Version: 3.x (for earlier see VERSION-2.x)
1212
#27: `@JacksonXmlElementWrapper` conflicting getter/setter definitions for property
1313
(reported by @n00bman)
1414
(fix by @cowtowncoder, w/ Claude code)
15+
#90: Add support for adding attributes to output for root element
16+
(fix by @cowtowncoder, w/ Claude code)
1517
#149: `@JacksonXmlElementWrapper` as a `@JsonCreator parameter` not working
1618
(reported by Dai M)
1719
(fix by Christopher M))
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tools.jackson.dataformat.xml.ser;
2+
3+
import java.util.Objects;
4+
5+
import javax.xml.namespace.QName;
6+
import javax.xml.stream.XMLStreamException;
7+
8+
import org.codehaus.stax2.XMLStreamWriter2;
9+
10+
import tools.jackson.dataformat.xml.util.ArgUtil;
11+
12+
/**
13+
* Value container to represent an attribute to add to the root XML element
14+
* being written, to be registered via
15+
* {@link XmlGeneratorInitializer#addRootAttribute(QName, String)}
16+
* (or its String-name overload).
17+
*<p>
18+
* Typical use case is adding XML Schema instance attributes such as
19+
* {@code xsi:schemaLocation} or {@code xsi:noNamespaceSchemaLocation},
20+
* but any attribute can be added.
21+
*<p>
22+
* NOTE: root attributes are only emitted when the root value being serialized
23+
* produces a structured (object) start element; scalar root values (e.g.
24+
* a bare {@code String}) do not currently get root attributes attached.
25+
*
26+
* @since 3.2
27+
*/
28+
public record RootAttribute(QName name, String value)
29+
implements XmlGeneratorWritable
30+
{
31+
public RootAttribute {
32+
Objects.requireNonNull(name, "name");
33+
ArgUtil.nonEmptyNonNull("name.localPart", name.getLocalPart());
34+
value = ArgUtil.nullToEmpty(value);
35+
}
36+
37+
@Override
38+
public void write(ToXmlGenerator xmlGen, XMLStreamWriter2 sw) throws XMLStreamException {
39+
final String ns = name.getNamespaceURI();
40+
if (ns == null || ns.isEmpty()) {
41+
sw.writeAttribute(name.getLocalPart(), value);
42+
} else {
43+
// Use prefix from QName if present; Stax will fall back to a bound
44+
// prefix when prefix is empty but namespace URI is registered
45+
// (e.g. via XmlGeneratorInitializer.addNamespace("xsi", ...)).
46+
sw.writeAttribute(name.getPrefix(), ns, name.getLocalPart(), value);
47+
}
48+
}
49+
}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ public class ToXmlGenerator
120120
*/
121121
protected List<PrologDirective> _prologDirectives;
122122

123+
/**
124+
* Attributes to add to the root element, if any.
125+
*
126+
* @since 3.2
127+
*/
128+
protected List<RootAttribute> _rootAttributes;
129+
123130
/**
124131
* Whether linefeed ("pretty-printing") enabled between directives
125132
* in Document prolog.
@@ -250,7 +257,8 @@ public ToXmlGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt,
250257
public void initDocument(XmlDeclaration xmlDeclaration,
251258
boolean lfBetweenPrologDirectives,
252259
List<PrologDirective> directives,
253-
List<NamespaceBinding> nsBindings)
260+
List<NamespaceBinding> nsBindings,
261+
List<RootAttribute> rootAttributes)
254262
{
255263
if (_initialized) { // sanity check
256264
_reportError("Internal error: cannot call `initDocument()` after generator already initialized");
@@ -259,6 +267,7 @@ public void initDocument(XmlDeclaration xmlDeclaration,
259267
_namespaceBindings = nsBindings;
260268
_lfBetweenPrologDirectives = lfBetweenPrologDirectives;
261269
_prologDirectives = directives;
270+
_rootAttributes = rootAttributes;
262271
}
263272

264273
/**
@@ -775,8 +784,13 @@ public final void _handleStartObject() throws JacksonException
775784

776785
// @since 3.2
777786
protected void _handleStartRootObject(QName rootElemName) throws XMLStreamException {
778-
// !!! TODO: special handling
779-
_xmlWriter.writeStartElement(_nextName.getNamespaceURI(), _nextName.getLocalPart());
787+
_xmlWriter.writeStartElement(rootElemName.getNamespaceURI(), rootElemName.getLocalPart());
788+
// [dataformat-xml#90]: emit caller-registered root attributes (e.g. xsi:schemaLocation)
789+
if (_rootAttributes != null) {
790+
for (RootAttribute attr : _rootAttributes) {
791+
attr.write(this, _xmlWriter);
792+
}
793+
}
780794
}
781795

782796
// note: public just because pretty printer needs to make a callback

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.util.ArrayList;
44
import java.util.List;
55

6+
import javax.xml.namespace.QName;
7+
68
import tools.jackson.core.JacksonException;
79
import tools.jackson.core.JsonGenerator;
810
import tools.jackson.core.exc.StreamWriteException;
@@ -47,6 +49,13 @@ public class XmlGeneratorInitializer
4749
*/
4850
protected List<NamespaceBinding> _namespaceBindings;
4951

52+
/**
53+
* Attributes to add to root element (if any).
54+
*
55+
* @since 3.2
56+
*/
57+
protected List<RootAttribute> _rootAttributes;
58+
5059
/**
5160
* Custom XML declaration to write.
5261
*/
@@ -61,7 +70,7 @@ public void initialize(SerializationConfig config, JsonGenerator g) throws Jacks
6170
if (g instanceof ToXmlGenerator xg) {
6271
xg.initDocument(_xmlDeclaration,
6372
_addLfBetweenPrologDirectives, _directives,
64-
_namespaceBindings);
73+
_namespaceBindings, _rootAttributes);
6574
}
6675
}
6776

@@ -178,6 +187,50 @@ public XmlGeneratorInitializer addNamespace(String prefix, String namespaceURI)
178187
return this;
179188
}
180189

190+
/**
191+
* Method for adding an attribute to be written on the root element of
192+
* the output document. Attributes are emitted in the order added,
193+
* after the root element's start tag is written and before any
194+
* content from the value being serialized.
195+
*<p>
196+
* Typical use case is adding XML Schema instance attributes such as
197+
* {@code xsi:schemaLocation} or {@code xsi:noNamespaceSchemaLocation}.
198+
*<p>
199+
* NOTE: root attributes are only emitted when the root value being
200+
* serialized produces a structured (object) start element; scalar
201+
* root values do not currently get root attributes attached.
202+
*
203+
* @param name Attribute name (with optional namespace and prefix)
204+
* @param value Attribute value (null is coerced to empty String)
205+
*
206+
* @return This initializer for call chaining
207+
*
208+
* @since 3.2
209+
*/
210+
public XmlGeneratorInitializer addRootAttribute(QName name, String value) {
211+
if (_rootAttributes == null) {
212+
_rootAttributes = new ArrayList<>();
213+
}
214+
_rootAttributes.add(new RootAttribute(name, value));
215+
return this;
216+
}
217+
218+
/**
219+
* Convenience overload of {@link #addRootAttribute(QName, String)} for
220+
* adding non-namespaced attribute by local name.
221+
*
222+
* @param localName Attribute local name (must be non-empty) in default
223+
* namespace (one with URI of "")
224+
* @param value Attribute value (if {@code null}, coerced to empty String)
225+
*
226+
* @return This initializer for call chaining
227+
*
228+
* @since 3.2
229+
*/
230+
public XmlGeneratorInitializer addRootAttribute(String localName, String value) {
231+
return addRootAttribute(new QName(localName), value);
232+
}
233+
181234
/**
182235
* Method for specifying custom XML declaration to write.
183236
*<p>

0 commit comments

Comments
 (0)