Skip to content

Commit 9d45c6b

Browse files
authored
Fix #452: add ability to write PIs in Prolog (#852)
1 parent c50ff1e commit 9d45c6b

5 files changed

Lines changed: 194 additions & 32 deletions

File tree

release-notes/CREDITS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ Lon Varscsak (@lonvarscsak)
7474
if element is in a collection
7575
(3.2.0)
7676

77+
Ondrej Zizka (@OndraZizka)
78+
* Requested #452: Allow adding `<?xml-stylesheet ...>` declarations (Processing
79+
Instructions)
80+
(3.2.0)
81+
7782
Jiri Mikulasek (@jimirocks)
7883
* Reported #455: Can't deserialize list in JsonSubtype when type property is visible
7984
(3.2.0)

release-notes/VERSION

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ Version: 3.x (for earlier see VERSION-2.x)
5454
if element is in a collection
5555
(reported by Lon V)
5656
(fix by @cowtowncoder, w/ Claude code)
57+
#452: Allow adding `<?xml-stylesheet ...>` declarations (Processing
58+
Instructions)
59+
(requested by Ondrej Z)
60+
(fix by @cowtowncoder)
5761
#455: Can't deserialize list in JsonSubtype when type property is visible
5862
(reported by Jiri M)
5963
(fix by @cowtowncoder, w/ Claude code)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 Processing Instruction (PI)
11+
* within Prolog part of the Document (before XML Root element,
12+
* after XML declaration if one written),
13+
* to be written using {@link XmlGeneratorInitializer}.
14+
*
15+
* @since 3.2
16+
*/
17+
public record PrologPI(String target, String data)
18+
implements PrologDirective
19+
{
20+
public PrologPI {
21+
target = ArgUtil.nonEmptyNonNull("target", target);
22+
data = ArgUtil.emptyToNull(data);
23+
}
24+
25+
@Override
26+
public void write(ToXmlGenerator xmlGen, XMLStreamWriter2 sw) throws XMLStreamException {
27+
if (data == null) {
28+
sw.writeProcessingInstruction(target);
29+
} else {
30+
sw.writeProcessingInstruction(target, data);
31+
}
32+
}
33+
}

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
*<ul>
1919
* <li>Document Type Declarations (DTD); that is "&lt;!DOCTYPE>" directive
2020
* </li>
21-
* <li>XML Comments (in Document prolog, before the root element)
21+
* <li>Comments (in Document prolog, before the root element)
22+
* </li>
23+
* <li>Processing Instructions (PIs; in Document prolog, before the root element)
2224
* </li>
2325
* </ul>
2426
*<p>
@@ -56,8 +58,8 @@ public XmlGeneratorInitializer linefeedsBetweenPrologDirectives(boolean addLFs)
5658
}
5759

5860
/**
59-
* Method for adding XML comment; to be written in position added
60-
* with respective to other directives
61+
* Method for adding XML comment; to be written at position added
62+
* relative to other directives
6163
* (but always after XML Declaration which must come before any other output;
6264
* and before Document Root element)
6365
*
@@ -89,7 +91,7 @@ public XmlGeneratorInitializer addDTD(String rootName,
8991

9092
/**
9193
* Method for adding Document Type Declaration (DTD) directive; to
92-
* be written in position added with respective to other directives
94+
* be written at position added relative to other directives
9395
* (but always after XML Declaration which must come before any other output;
9496
* and before Document Root element)
9597
*
@@ -105,6 +107,23 @@ public XmlGeneratorInitializer addDTD(DTD dtd) {
105107
return _add(dtd);
106108
}
107109

110+
/**
111+
* Method for adding XML Processing Instruction (PI); to be written at
112+
* position added relative to other directives
113+
* (but always after XML Declaration which must come before any other output;
114+
* and before Document Root element)
115+
*
116+
* @param target Processing Instruction target: must not be {@code null} or
117+
* empty String
118+
* @param data (optional) Processing Instruction data part, if any,
119+
* separated by a space from target (if not null)
120+
*
121+
* @return This initializer for call chaining
122+
*/
123+
public XmlGeneratorInitializer addPI(String target, String data) {
124+
return _add(new PrologPI(target, data));
125+
}
126+
108127
protected XmlGeneratorInitializer _add(PrologDirective d) {
109128
if (_directives == null) {
110129
_directives = new ArrayList<>();

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

Lines changed: 129 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ public class XmlGeneratorInitializerTest extends XmlTestUtil
1919
@Test
2020
public void testDTDWithOnlyRootElement() throws Exception
2121
{
22-
ObjectWriter w = MAPPER.writer().with(
23-
new XmlGeneratorInitializer()
22+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
2423
.addDTD("StringBean", null, null, null));
2524
assertEquals(a2q("<!DOCTYPE StringBean>\n"
2625
+"<StringBean><text>test</text></StringBean>"),
@@ -30,8 +29,7 @@ public void testDTDWithOnlyRootElement() throws Exception
3029
@Test
3130
public void testDTDWithPublicId() throws Exception
3231
{
33-
ObjectWriter w = MAPPER.writer().with(
34-
new XmlGeneratorInitializer()
32+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
3533
.addDTD("StringBean", "system", "http://foo.bar", ""));
3634
assertEquals(a2q("<!DOCTYPE StringBean PUBLIC 'http://foo.bar' 'system'>\n"
3735
+"<StringBean><text>test</text></StringBean>"),
@@ -41,8 +39,7 @@ public void testDTDWithPublicId() throws Exception
4139
@Test
4240
public void testDTDWithSystemIdOnly() throws Exception
4341
{
44-
ObjectWriter w = MAPPER.writer().with(
45-
new XmlGeneratorInitializer()
42+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
4643
.addDTD("StringBean", "system", "", null));
4744
assertEquals(a2q("<!DOCTYPE StringBean SYSTEM 'system'>\n"
4845
+"<StringBean><text>test</text></StringBean>"),
@@ -52,8 +49,7 @@ public void testDTDWithSystemIdOnly() throws Exception
5249
@Test
5350
public void testDTDWithInternalSubset() throws Exception
5451
{
55-
ObjectWriter w = MAPPER.writer().with(
56-
new XmlGeneratorInitializer()
52+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
5753
.addDTD("StringBean", "system", "http://foo.bar", "<!ELEMENT root (#PCDATA)>"));
5854
assertEquals(a2q("<!DOCTYPE StringBean PUBLIC 'http://foo.bar' 'system' "
5955
+"[<!ELEMENT root (#PCDATA)>]>\n"
@@ -68,8 +64,7 @@ public void testDTDWithXmlDeclaration() throws Exception
6864
XmlMapper mapper = XmlMapper.builder()
6965
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
7066
.build();
71-
ObjectWriter w = mapper.writer().with(
72-
new XmlGeneratorInitializer()
67+
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
7368
.addDTD("StringBean", "system", "http://foo.bar", null));
7469
// XML declaration is emitted with single quotes, DOCTYPE with double quotes,
7570
// so cannot use a2q() on the whole string here.
@@ -84,8 +79,7 @@ public void testDTDWithXmlDeclaration() throws Exception
8479
public void testDTDInvalidNoRoot() throws Exception
8580
{
8681
try {
87-
/*ObjectWriter w =*/ MAPPER.writer().with(
88-
new XmlGeneratorInitializer()
82+
/*ObjectWriter w =*/ _writer(new XmlGeneratorInitializer()
8983
.addDTD("", null, null, null));
9084
fail("Should not pass");
9185
} catch (IllegalArgumentException e) {
@@ -98,8 +92,7 @@ public void testDTDInvalidNoRoot() throws Exception
9892
@Test
9993
public void testSimpleComment() throws Exception
10094
{
101-
ObjectWriter w = MAPPER.writer().with(
102-
new XmlGeneratorInitializer()
95+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
10396
.addComment("Comment content!"));
10497
assertEquals(a2q("<!--Comment content!-->\n"
10598
+"<StringBean><text>test</text></StringBean>"),
@@ -113,8 +106,7 @@ public void testCommentWithXmlDeclaration() throws Exception
113106
XmlMapper mapper = XmlMapper.builder()
114107
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
115108
.build();
116-
ObjectWriter w = mapper.writer().with(
117-
new XmlGeneratorInitializer()
109+
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
118110
.addComment("Hello"));
119111
// XML declaration is emitted with single quotes, so cannot use a2q() here.
120112
assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n"
@@ -127,8 +119,7 @@ public void testCommentWithXmlDeclaration() throws Exception
127119
@Test
128120
public void testCommentBeforeDTD() throws Exception
129121
{
130-
ObjectWriter w = MAPPER.writer().with(
131-
new XmlGeneratorInitializer()
122+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
132123
.addComment("before dtd")
133124
.addDTD("StringBean", null, null, null));
134125
assertEquals(a2q("<!--before dtd-->\n"
@@ -141,8 +132,7 @@ public void testCommentBeforeDTD() throws Exception
141132
@Test
142133
public void testDTDBeforeComment() throws Exception
143134
{
144-
ObjectWriter w = MAPPER.writer().with(
145-
new XmlGeneratorInitializer()
135+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
146136
.addDTD("StringBean", null, null, null)
147137
.addComment("after dtd"));
148138
assertEquals(a2q("<!DOCTYPE StringBean>\n"
@@ -155,8 +145,7 @@ public void testDTDBeforeComment() throws Exception
155145
@Test
156146
public void testMultipleComments() throws Exception
157147
{
158-
ObjectWriter w = MAPPER.writer().with(
159-
new XmlGeneratorInitializer()
148+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
160149
.addComment("first")
161150
.addComment("second")
162151
.addComment("third"));
@@ -171,8 +160,7 @@ public void testMultipleComments() throws Exception
171160
@Test
172161
public void testEmptyComment() throws Exception
173162
{
174-
ObjectWriter w = MAPPER.writer().with(
175-
new XmlGeneratorInitializer()
163+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
176164
.addComment(""));
177165
assertEquals(a2q("<!---->\n"
178166
+"<StringBean><text>test</text></StringBean>"),
@@ -183,8 +171,7 @@ public void testEmptyComment() throws Exception
183171
@Test
184172
public void testNullComment() throws Exception
185173
{
186-
ObjectWriter w = MAPPER.writer().with(
187-
new XmlGeneratorInitializer()
174+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
188175
.addComment(null));
189176
assertEquals(a2q("<!---->\n"
190177
+"<StringBean><text>test</text></StringBean>"),
@@ -199,8 +186,7 @@ public void testLinefeedsBetweenPrologDirectivesDisabled() throws Exception
199186
XmlMapper mapper = XmlMapper.builder()
200187
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
201188
.build();
202-
ObjectWriter w = mapper.writer().with(
203-
new XmlGeneratorInitializer()
189+
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
204190
.linefeedsBetweenPrologDirectives(false)
205191
.addDTD("StringBean", null, null, null)
206192
.addComment("squished"));
@@ -212,5 +198,120 @@ public void testLinefeedsBetweenPrologDirectivesDisabled() throws Exception
212198
w.writeValueAsString(new StringBean("test")));
213199
}
214200

201+
// // [dataformat-xml#452]: PI writing -- ok cases
202+
203+
@Test
204+
public void testSimplePIs() throws Exception
205+
{
206+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
207+
.addPI("target", "data"));
208+
assertEquals(a2q("<?target data?>\n"
209+
+"<StringBean><text>test</text></StringBean>"),
210+
w.writeValueAsString(new StringBean("test")));
211+
212+
// Then empty/null
213+
final String EXP_WITH_NO_DATA = a2q("<?target?>\n"
214+
+"<StringBean><text>test</text></StringBean>");
215+
216+
w = _writer(new XmlGeneratorInitializer()
217+
.addPI("target", ""));
218+
assertEquals(EXP_WITH_NO_DATA,
219+
w.writeValueAsString(new StringBean("test")));
220+
221+
w = _writer(new XmlGeneratorInitializer()
222+
.addPI("target", null));
223+
assertEquals(EXP_WITH_NO_DATA,
224+
w.writeValueAsString(new StringBean("test")));
225+
}
226+
227+
// Ensure multiple PIs are all written in insertion order
228+
@Test
229+
public void testMultiplePIs() throws Exception
230+
{
231+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
232+
.addPI("xml-stylesheet", "type=\"text/xsl\" href=\"style.xsl\"")
233+
.addPI("target2", "data2"));
234+
assertEquals(a2q("<?xml-stylesheet type=\"text/xsl\" href=\"style.xsl\"?>\n"
235+
+"<?target2 data2?>\n"
236+
+"<StringBean><text>test</text></StringBean>"),
237+
w.writeValueAsString(new StringBean("test")));
238+
}
239+
240+
// Verify ordering: XML declaration must come before PI
241+
@Test
242+
public void testPIWithXmlDeclaration() throws Exception
243+
{
244+
XmlMapper mapper = XmlMapper.builder()
245+
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
246+
.build();
247+
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
248+
.addPI("target", "data"));
249+
// XML declaration is emitted with single quotes, so cannot use a2q() here.
250+
assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n"
251+
+"<?target data?>\n"
252+
+"<StringBean><text>test</text></StringBean>",
253+
w.writeValueAsString(new StringBean("test")));
254+
}
255+
256+
// Verify "position added" ordering contract: Comment before PI
257+
@Test
258+
public void testCommentBeforePI() throws Exception
259+
{
260+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
261+
.addComment("before pi")
262+
.addPI("target", "data"));
263+
assertEquals(a2q("<!--before pi-->\n"
264+
+"<?target data?>\n"
265+
+"<StringBean><text>test</text></StringBean>"),
266+
w.writeValueAsString(new StringBean("test")));
267+
}
268+
269+
// Verify "position added" ordering contract: PI before Comment
270+
@Test
271+
public void testPIBeforeComment() throws Exception
272+
{
273+
ObjectWriter w = _writer(new XmlGeneratorInitializer()
274+
.addPI("target", "data")
275+
.addComment("after pi"));
276+
assertEquals(a2q("<?target data?>\n"
277+
+"<!--after pi-->\n"
278+
+"<StringBean><text>test</text></StringBean>"),
279+
w.writeValueAsString(new StringBean("test")));
280+
}
281+
282+
// // [dataformat-xml#452]: PI writing -- failing cases
283+
284+
@Test
285+
public void testInvalidPINullTarget() throws Exception
286+
{
287+
try {
288+
/*ObjectWriter w =*/ _writer(new XmlGeneratorInitializer()
289+
.addPI(null, "data"));
290+
fail("Should not pass");
291+
} catch (IllegalArgumentException e) {
292+
verifyException(e, "Illegal argument for 'target': must be");
293+
}
294+
}
295+
296+
@Test
297+
public void testInvalidPIEmptyTarget() throws Exception
298+
{
299+
try {
300+
/*ObjectWriter w =*/ _writer(new XmlGeneratorInitializer()
301+
.addPI("", "data"));
302+
fail("Should not pass");
303+
} catch (IllegalArgumentException e) {
304+
verifyException(e, "Illegal argument for 'target': must be");
305+
}
306+
}
307+
215308
// // Other tests
309+
310+
private ObjectWriter _writer(XmlGeneratorInitializer initializer) {
311+
return _writer(MAPPER, initializer);
312+
}
313+
314+
private ObjectWriter _writer(XmlMapper mapper, XmlGeneratorInitializer initializer) {
315+
return mapper.writer().with(initializer);
316+
}
216317
}

0 commit comments

Comments
 (0)