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
5 changes: 5 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ Lon Varscsak (@lonvarscsak)
if element is in a collection
(3.2.0)

Ondrej Zizka (@OndraZizka)
* Requested #452: Allow adding `<?xml-stylesheet ...>` declarations (Processing
Instructions)
(3.2.0)

Jiri Mikulasek (@jimirocks)
* Reported #455: Can't deserialize list in JsonSubtype when type property is visible
(3.2.0)
Expand Down
4 changes: 4 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Version: 3.x (for earlier see VERSION-2.x)
if element is in a collection
(reported by Lon V)
(fix by @cowtowncoder, w/ Claude code)
#452: Allow adding `<?xml-stylesheet ...>` declarations (Processing
Instructions)
(requested by Ondrej Z)
(fix by @cowtowncoder)
#455: Can't deserialize list in JsonSubtype when type property is visible
(reported by Jiri M)
(fix by @cowtowncoder, w/ Claude code)
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/tools/jackson/dataformat/xml/ser/PrologPI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 Processing Instruction (PI)
* 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 PrologPI(String target, String data)
implements PrologDirective
{
public PrologPI {
target = ArgUtil.nonEmptyNonNull("target", target);
data = ArgUtil.emptyToNull(data);
}

@Override
public void write(ToXmlGenerator xmlGen, XMLStreamWriter2 sw) throws XMLStreamException {
if (data == null) {
sw.writeProcessingInstruction(target);
} else {
sw.writeProcessingInstruction(target, data);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
*<ul>
* <li>Document Type Declarations (DTD); that is "&lt;!DOCTYPE>" directive
* </li>
* <li>XML Comments (in Document prolog, before the root element)
* <li>Comments (in Document prolog, before the root element)
* </li>
* <li>Processing Instructions (PIs; in Document prolog, before the root element)
* </li>
* </ul>
*<p>
Expand Down Expand Up @@ -56,8 +58,8 @@ public XmlGeneratorInitializer linefeedsBetweenPrologDirectives(boolean addLFs)
}

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

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

/**
* Method for adding XML Processing Instruction (PI); to be written at
* position added relative to other directives
* (but always after XML Declaration which must come before any other output;
* and before Document Root element)
*
* @param target Processing Instruction target: must not be {@code null} or
* empty String
* @param data (optional) Processing Instruction data part, if any,
* separated by a space from target (if not null)
*
* @return This initializer for call chaining
*/
public XmlGeneratorInitializer addPI(String target, String data) {
return _add(new PrologPI(target, data));
}

protected XmlGeneratorInitializer _add(PrologDirective d) {
if (_directives == null) {
_directives = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ public class XmlGeneratorInitializerTest extends XmlTestUtil
@Test
public void testDTDWithOnlyRootElement() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addDTD("StringBean", null, null, null));
assertEquals(a2q("<!DOCTYPE StringBean>\n"
+"<StringBean><text>test</text></StringBean>"),
Expand All @@ -30,8 +29,7 @@ public void testDTDWithOnlyRootElement() throws Exception
@Test
public void testDTDWithPublicId() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addDTD("StringBean", "system", "http://foo.bar", ""));
assertEquals(a2q("<!DOCTYPE StringBean PUBLIC 'http://foo.bar' 'system'>\n"
+"<StringBean><text>test</text></StringBean>"),
Expand All @@ -41,8 +39,7 @@ public void testDTDWithPublicId() throws Exception
@Test
public void testDTDWithSystemIdOnly() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addDTD("StringBean", "system", "", null));
assertEquals(a2q("<!DOCTYPE StringBean SYSTEM 'system'>\n"
+"<StringBean><text>test</text></StringBean>"),
Expand All @@ -52,8 +49,7 @@ public void testDTDWithSystemIdOnly() throws Exception
@Test
public void testDTDWithInternalSubset() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addDTD("StringBean", "system", "http://foo.bar", "<!ELEMENT root (#PCDATA)>"));
assertEquals(a2q("<!DOCTYPE StringBean PUBLIC 'http://foo.bar' 'system' "
+"[<!ELEMENT root (#PCDATA)>]>\n"
Expand All @@ -68,8 +64,7 @@ public void testDTDWithXmlDeclaration() throws Exception
XmlMapper mapper = XmlMapper.builder()
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
.build();
ObjectWriter w = mapper.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
.addDTD("StringBean", "system", "http://foo.bar", null));
// XML declaration is emitted with single quotes, DOCTYPE with double quotes,
// so cannot use a2q() on the whole string here.
Expand All @@ -84,8 +79,7 @@ public void testDTDWithXmlDeclaration() throws Exception
public void testDTDInvalidNoRoot() throws Exception
{
try {
/*ObjectWriter w =*/ MAPPER.writer().with(
new XmlGeneratorInitializer()
/*ObjectWriter w =*/ _writer(new XmlGeneratorInitializer()
.addDTD("", null, null, null));
fail("Should not pass");
} catch (IllegalArgumentException e) {
Expand All @@ -98,8 +92,7 @@ public void testDTDInvalidNoRoot() throws Exception
@Test
public void testSimpleComment() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addComment("Comment content!"));
assertEquals(a2q("<!--Comment content!-->\n"
+"<StringBean><text>test</text></StringBean>"),
Expand All @@ -113,8 +106,7 @@ public void testCommentWithXmlDeclaration() throws Exception
XmlMapper mapper = XmlMapper.builder()
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
.build();
ObjectWriter w = mapper.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
.addComment("Hello"));
// XML declaration is emitted with single quotes, so cannot use a2q() here.
assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n"
Expand All @@ -127,8 +119,7 @@ public void testCommentWithXmlDeclaration() throws Exception
@Test
public void testCommentBeforeDTD() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addComment("before dtd")
.addDTD("StringBean", null, null, null));
assertEquals(a2q("<!--before dtd-->\n"
Expand All @@ -141,8 +132,7 @@ public void testCommentBeforeDTD() throws Exception
@Test
public void testDTDBeforeComment() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addDTD("StringBean", null, null, null)
.addComment("after dtd"));
assertEquals(a2q("<!DOCTYPE StringBean>\n"
Expand All @@ -155,8 +145,7 @@ public void testDTDBeforeComment() throws Exception
@Test
public void testMultipleComments() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addComment("first")
.addComment("second")
.addComment("third"));
Expand All @@ -171,8 +160,7 @@ public void testMultipleComments() throws Exception
@Test
public void testEmptyComment() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addComment(""));
assertEquals(a2q("<!---->\n"
+"<StringBean><text>test</text></StringBean>"),
Expand All @@ -183,8 +171,7 @@ public void testEmptyComment() throws Exception
@Test
public void testNullComment() throws Exception
{
ObjectWriter w = MAPPER.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addComment(null));
assertEquals(a2q("<!---->\n"
+"<StringBean><text>test</text></StringBean>"),
Expand All @@ -199,8 +186,7 @@ public void testLinefeedsBetweenPrologDirectivesDisabled() throws Exception
XmlMapper mapper = XmlMapper.builder()
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
.build();
ObjectWriter w = mapper.writer().with(
new XmlGeneratorInitializer()
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
.linefeedsBetweenPrologDirectives(false)
.addDTD("StringBean", null, null, null)
.addComment("squished"));
Expand All @@ -212,5 +198,120 @@ public void testLinefeedsBetweenPrologDirectivesDisabled() throws Exception
w.writeValueAsString(new StringBean("test")));
}

// // [dataformat-xml#452]: PI writing -- ok cases

@Test
public void testSimplePIs() throws Exception
{
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addPI("target", "data"));
assertEquals(a2q("<?target data?>\n"
+"<StringBean><text>test</text></StringBean>"),
w.writeValueAsString(new StringBean("test")));

// Then empty/null
final String EXP_WITH_NO_DATA = a2q("<?target?>\n"
+"<StringBean><text>test</text></StringBean>");

w = _writer(new XmlGeneratorInitializer()
.addPI("target", ""));
assertEquals(EXP_WITH_NO_DATA,
w.writeValueAsString(new StringBean("test")));

w = _writer(new XmlGeneratorInitializer()
.addPI("target", null));
assertEquals(EXP_WITH_NO_DATA,
w.writeValueAsString(new StringBean("test")));
}

// Ensure multiple PIs are all written in insertion order
@Test
public void testMultiplePIs() throws Exception
{
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addPI("xml-stylesheet", "type=\"text/xsl\" href=\"style.xsl\"")
.addPI("target2", "data2"));
assertEquals(a2q("<?xml-stylesheet type=\"text/xsl\" href=\"style.xsl\"?>\n"
+"<?target2 data2?>\n"
+"<StringBean><text>test</text></StringBean>"),
w.writeValueAsString(new StringBean("test")));
}

// Verify ordering: XML declaration must come before PI
@Test
public void testPIWithXmlDeclaration() throws Exception
{
XmlMapper mapper = XmlMapper.builder()
.configure(XmlWriteFeature.WRITE_XML_DECLARATION, true)
.build();
ObjectWriter w = _writer(mapper, new XmlGeneratorInitializer()
.addPI("target", "data"));
// XML declaration is emitted with single quotes, so cannot use a2q() here.
assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n"
+"<?target data?>\n"
+"<StringBean><text>test</text></StringBean>",
w.writeValueAsString(new StringBean("test")));
}

// Verify "position added" ordering contract: Comment before PI
@Test
public void testCommentBeforePI() throws Exception
{
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addComment("before pi")
.addPI("target", "data"));
assertEquals(a2q("<!--before pi-->\n"
+"<?target data?>\n"
+"<StringBean><text>test</text></StringBean>"),
w.writeValueAsString(new StringBean("test")));
}

// Verify "position added" ordering contract: PI before Comment
@Test
public void testPIBeforeComment() throws Exception
{
ObjectWriter w = _writer(new XmlGeneratorInitializer()
.addPI("target", "data")
.addComment("after pi"));
assertEquals(a2q("<?target data?>\n"
+"<!--after pi-->\n"
+"<StringBean><text>test</text></StringBean>"),
w.writeValueAsString(new StringBean("test")));
}

// // [dataformat-xml#452]: PI writing -- failing cases

@Test
public void testInvalidPINullTarget() throws Exception
{
try {
/*ObjectWriter w =*/ _writer(new XmlGeneratorInitializer()
.addPI(null, "data"));
fail("Should not pass");
} catch (IllegalArgumentException e) {
verifyException(e, "Illegal argument for 'target': must be");
}
}

@Test
public void testInvalidPIEmptyTarget() throws Exception
{
try {
/*ObjectWriter w =*/ _writer(new XmlGeneratorInitializer()
.addPI("", "data"));
fail("Should not pass");
} catch (IllegalArgumentException e) {
verifyException(e, "Illegal argument for 'target': must be");
}
}

// // Other tests

private ObjectWriter _writer(XmlGeneratorInitializer initializer) {
return _writer(MAPPER, initializer);
}

private ObjectWriter _writer(XmlMapper mapper, XmlGeneratorInitializer initializer) {
return mapper.writer().with(initializer);
}
}