From fbe88c1197f79d8243f7153ab528a73d1d6500e1 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:53:45 +0700 Subject: [PATCH] Fix Jaxb2Marshaller MTOM with schema validation When mtomEnabled and a Schema are both configured, validate the logical infoset first (non-XOP base64Binary simple content, SWA placeholders), then MTOM-encode without Schema so xop:Include children do not fail cvc-type.3.1.2. Fixes spring-projects/spring-ws#1030 (SWS-958). Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- .../oxm/jaxb/Jaxb2Marshaller.java | 66 ++++++++++++++++++- .../oxm/jaxb/Jaxb2MarshallerTests.java | 45 +++++++++++++ .../oxm/jaxb/binary-object.xsd | 17 +++++ 3 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 spring-oxm/src/test/resources/org/springframework/oxm/jaxb/binary-object.xsd diff --git a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java index 32a953814141..5e273d175f68 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java @@ -53,6 +53,7 @@ import javax.xml.stream.XMLStreamWriter; import javax.xml.transform.Result; import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stax.StAXSource; @@ -705,7 +706,26 @@ public void marshal(Object graph, Result result) throws XmlMappingException { @Override public void marshal(Object graph, Result result, @Nullable MimeContainer mimeContainer) throws XmlMappingException { try { - Marshaller marshaller = createMarshaller(); + // XOP packaging replaces base64Binary simple content with xop:Include children. + // Schema validation must therefore run against the logical (non-XOP) infoset first, + // then MTOM marshalling proceeds without a Schema so validation does not see XOP. + // See spring-projects/spring-ws#1030 / SWS-958. + if (this.mtomEnabled && mimeContainer != null && this.schema != null) { + Marshaller validatingMarshaller = createMarshaller(true); + // Non-XOP marshaller keeps base64 as simple content and supports @XmlAttachmentRef. + validatingMarshaller.setAttachmentMarshaller(LogicalAttachmentMarshaller.INSTANCE); + validatingMarshaller.marshal(graph, new DOMResult()); + Marshaller mtomMarshaller = createMarshaller(false); + mtomMarshaller.setAttachmentMarshaller(new Jaxb2AttachmentMarshaller(mimeContainer)); + if (StaxUtils.isStaxResult(result)) { + marshalStaxResult(mtomMarshaller, graph, result); + } + else { + mtomMarshaller.marshal(graph, result); + } + return; + } + Marshaller marshaller = createMarshaller(true); if (this.mtomEnabled && mimeContainer != null) { marshaller.setAttachmentMarshaller(new Jaxb2AttachmentMarshaller(mimeContainer)); } @@ -729,9 +749,13 @@ public void marshal(Object graph, Result result, @Nullable MimeContainer mimeCon * @see #createUnmarshaller() */ public Marshaller createMarshaller() { + return createMarshaller(true); + } + + private Marshaller createMarshaller(boolean applySchema) { try { Marshaller marshaller = getJaxbContext().createMarshaller(); - initJaxbMarshaller(marshaller); + initJaxbMarshaller(marshaller, applySchema); return marshaller; } catch (JAXBException ex) { @@ -766,6 +790,10 @@ private void marshalStaxResult(Marshaller jaxbMarshaller, Object graph, Result s * and {@link #setAdapters adapters}. */ protected void initJaxbMarshaller(Marshaller marshaller) throws JAXBException { + initJaxbMarshaller(marshaller, true); + } + + private void initJaxbMarshaller(Marshaller marshaller, boolean applySchema) throws JAXBException { if (this.marshallerProperties != null) { for (Map.Entry entry : this.marshallerProperties.entrySet()) { marshaller.setProperty(entry.getKey(), entry.getValue()); @@ -782,7 +810,7 @@ protected void initJaxbMarshaller(Marshaller marshaller) throws JAXBException { marshaller.setAdapter(adapter); } } - if (this.schema != null) { + if (applySchema && this.schema != null) { marshaller.setSchema(this.schema); } } @@ -983,6 +1011,38 @@ else if (ex instanceof UnmarshalException) { } + /** + * Attachment marshaller used only while validating the logical infoset (no XOP). + * {@link #isXOPPackage()} is {@code false} so {@code xs:base64Binary} stays simple + * content; SWA/{@code @XmlAttachmentRef} fields still get a placeholder content id. + */ + private static final class LogicalAttachmentMarshaller extends AttachmentMarshaller { + + static final LogicalAttachmentMarshaller INSTANCE = new LogicalAttachmentMarshaller(); + + @Override + public boolean isXOPPackage() { + return false; + } + + @Override + public String addMtomAttachment(byte[] data, int offset, int length, String mimeType, + String elementNamespace, String elementLocalName) { + return "cid:validation"; + } + + @Override + public String addMtomAttachment(DataHandler dataHandler, String elementNamespace, String elementLocalName) { + return "cid:validation"; + } + + @Override + public String addSwaRefAttachment(DataHandler dataHandler) { + return "cid:validation"; + } + } + + private static class Jaxb2AttachmentMarshaller extends AttachmentMarshaller { private final MimeContainer mimeContainer; diff --git a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java index 0544c02cbb34..0550e8816d7a 100644 --- a/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java +++ b/spring-oxm/src/test/java/org/springframework/oxm/jaxb/Jaxb2MarshallerTests.java @@ -45,6 +45,7 @@ import org.xmlunit.diff.DifferenceEvaluator; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.testfixture.xml.XmlContent; import org.springframework.oxm.AbstractMarshallerTests; @@ -291,6 +292,50 @@ void marshalAttachments() throws Exception { verify(mimeContainer, times(3)).addAttachment(isA(String.class), isA(DataHandler.class)); } + @Test // spring-ws#1030 / SWS-958: mtomEnabled + schema must validate logical infoset then XOP-encode + void marshalAttachmentsWithSchema() throws Exception { + marshaller = new Jaxb2Marshaller(); + marshaller.setClassesToBeBound(BinaryObject.class); + marshaller.setMtomEnabled(true); + marshaller.setSchema(new ClassPathResource("binary-object.xsd", getClass())); + marshaller.afterPropertiesSet(); + MimeContainer mimeContainer = mock(); + + Resource logo = new ClassPathResource("spring-ws.png", getClass()); + DataHandler dataHandler = new DataHandler(new FileDataSource(logo.getFile())); + + given(mimeContainer.convertToXopPackage()).willReturn(true); + byte[] bytes = FileCopyUtils.copyToByteArray(logo.getInputStream()); + BinaryObject object = new BinaryObject(bytes, dataHandler); + StringWriter writer = new StringWriter(); + marshaller.marshal(object, new StreamResult(writer), mimeContainer); + String xml = writer.toString(); + assertThat(xml).as("No XML written").isNotEmpty(); + assertThat(xml).contains("Include"); + verify(mimeContainer, times(3)).addAttachment(isA(String.class), isA(DataHandler.class)); + } + + @Test // spring-ws#1030: schema still rejects content that does not match when MTOM is enabled + void marshalAttachmentsWithSchemaRejectsInvalidGraph() throws Exception { + marshaller = new Jaxb2Marshaller(); + marshaller.setClassesToBeBound(BinaryObject.class); + marshaller.setMtomEnabled(true); + // flight.xsd does not describe BinaryObject — logical validation must still fail before XOP + marshaller.setSchema(new FileSystemResource("src/test/schema/flight.xsd")); + marshaller.afterPropertiesSet(); + MimeContainer mimeContainer = mock(); + + Resource logo = new ClassPathResource("spring-ws.png", getClass()); + DataHandler dataHandler = new DataHandler(new FileDataSource(logo.getFile())); + given(mimeContainer.convertToXopPackage()).willReturn(true); + byte[] bytes = FileCopyUtils.copyToByteArray(logo.getInputStream()); + BinaryObject object = new BinaryObject(bytes, dataHandler); + + assertThatExceptionOfType(XmlMappingException.class).isThrownBy(() -> + marshaller.marshal(object, new StreamResult(new StringWriter()), mimeContainer)); + verify(mimeContainer, times(0)).addAttachment(isA(String.class), isA(DataHandler.class)); + } + @Test // SPR-10714 void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception { marshaller = new Jaxb2Marshaller(); diff --git a/spring-oxm/src/test/resources/org/springframework/oxm/jaxb/binary-object.xsd b/spring-oxm/src/test/resources/org/springframework/oxm/jaxb/binary-object.xsd new file mode 100644 index 000000000000..2ee133394e49 --- /dev/null +++ b/spring-oxm/src/test/resources/org/springframework/oxm/jaxb/binary-object.xsd @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +