/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.oxm.jaxb;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.oxm.AbstractMarshallerTests;
import org.springframework.oxm.UncategorizedMappingException;
import org.springframework.oxm.XmlMappingException;
import org.springframework.oxm.jaxb.test.FlightType;
import org.springframework.oxm.jaxb.test.Flights;
import org.springframework.oxm.jaxb.test.ObjectFactory;
import org.springframework.oxm.mime.MimeContainer;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ReflectionUtils;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xmlunit.diff.DifferenceEvaluator;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.namespace.QName;
import javax.xml.transform.Result;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collections;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.BDDMockito.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.inOrder;
import static org.mockito.BDDMockito.isA;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.reset;
import static org.mockito.BDDMockito.times;
import static org.mockito.BDDMockito.verify;
import static org.xmlunit.diff.ComparisonType.XML_STANDALONE;
import static org.xmlunit.diff.DifferenceEvaluators.Default;
import static org.xmlunit.diff.DifferenceEvaluators.chain;
import static org.xmlunit.diff.DifferenceEvaluators.downgradeDifferencesToEqual;
import static org.xmlunit.matchers.CompareMatcher.isSimilarTo;
/**
* @author Arjen Poutsma
* @author Biju Kunjummen
* @author Sam Brannen
*/
public class Jaxb2MarshallerTests extends AbstractMarshallerTests<Jaxb2Marshaller> {
private static final String CONTEXT_PATH = "org.springframework.oxm.jaxb.test";
private Flights flights;
@Override
protected Jaxb2Marshaller createMarshaller() throws Exception {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath(CONTEXT_PATH);
marshaller.afterPropertiesSet();
return marshaller;
}
@Override
protected Object createFlights() {
FlightType flight = new FlightType();
flight.setNumber(42L);
flights = new Flights();
flights.getFlight().add(flight);
return flights;
}
@Test
public void marshalSAXResult() throws Exception {
ContentHandler contentHandler = mock(ContentHandler.class);
SAXResult result = new SAXResult(contentHandler);
marshaller.marshal(flights, result);
InOrder ordered = inOrder(contentHandler);
ordered.verify(contentHandler).setDocumentLocator(isA(Locator.class));
ordered.verify(contentHandler).startDocument();
ordered.verify(contentHandler).startPrefixMapping("", "http://samples.springframework.org/flight");
ordered.verify(contentHandler).startElement(eq("http://samples.springframework.org/flight"), eq("flights"), eq("flights"), isA(Attributes.class));
ordered.verify(contentHandler).startElement(eq("http://samples.springframework.org/flight"), eq("flight"), eq("flight"), isA(Attributes.class));
ordered.verify(contentHandler).startElement(eq("http://samples.springframework.org/flight"), eq("number"), eq("number"), isA(Attributes.class));
ordered.verify(contentHandler).characters(isA(char[].class), eq(0), eq(2));
ordered.verify(contentHandler).endElement("http://samples.springframework.org/flight", "number", "number");
ordered.verify(contentHandler).endElement("http://samples.springframework.org/flight", "flight", "flight");
ordered.verify(contentHandler).endElement("http://samples.springframework.org/flight", "flights", "flights");
ordered.verify(contentHandler).endPrefixMapping("");
ordered.verify(contentHandler).endDocument();
}
@Test
public void lazyInit() throws Exception {
marshaller = new Jaxb2Marshaller();
marshaller.setContextPath(CONTEXT_PATH);
marshaller.setLazyInit(true);
marshaller.afterPropertiesSet();
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
marshaller.marshal(flights, result);
DifferenceEvaluator ev = chain(Default, downgradeDifferencesToEqual(XML_STANDALONE));
assertThat("Marshaller writes invalid StreamResult", writer.toString(),
isSimilarTo(EXPECTED_STRING).withDifferenceEvaluator(ev));
}
@Test
public void properties() throws Exception {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath(CONTEXT_PATH);
marshaller.setMarshallerProperties(
Collections.<String, Object>singletonMap(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT,
Boolean.TRUE));
marshaller.afterPropertiesSet();
}
@Test(expected = IllegalArgumentException.class)
public void noContextPathOrClassesToBeBound() throws Exception {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.afterPropertiesSet();
}
@Test(expected = UncategorizedMappingException.class)
public void testInvalidContextPath() throws Exception {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("ab");
marshaller.afterPropertiesSet();
}
@Test(expected = XmlMappingException.class)
public void marshalInvalidClass() throws Exception {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(FlightType.class);
marshaller.afterPropertiesSet();
Result result = new StreamResult(new StringWriter());
Flights flights = new Flights();
marshaller.marshal(flights, result);
}
@Test
public void supportsContextPath() throws Exception {
testSupports();
}
@Test
public void supportsClassesToBeBound() throws Exception {
marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(Flights.class, FlightType.class);
marshaller.afterPropertiesSet();
testSupports();
}
@Test
public void supportsPackagesToScan() throws Exception {
marshaller = new Jaxb2Marshaller();
marshaller.setPackagesToScan(new String[] {CONTEXT_PATH});
marshaller.afterPropertiesSet();
testSupports();
}
private void testSupports() throws Exception {
assertTrue("Jaxb2Marshaller does not support Flights class", marshaller.supports(Flights.class));
assertTrue("Jaxb2Marshaller does not support Flights generic type", marshaller.supports((Type)Flights.class));
assertFalse("Jaxb2Marshaller supports FlightType class", marshaller.supports(FlightType.class));
assertFalse("Jaxb2Marshaller supports FlightType type", marshaller.supports((Type)FlightType.class));
Method method = ObjectFactory.class.getDeclaredMethod("createFlight", FlightType.class);
assertTrue("Jaxb2Marshaller does not support JAXBElement<FlightsType>",
marshaller.supports(method.getGenericReturnType()));
marshaller.setSupportJaxbElementClass(true);
JAXBElement<FlightType> flightTypeJAXBElement = new JAXBElement<>(new QName("http://springframework.org", "flight"), FlightType.class,
new FlightType());
assertTrue("Jaxb2Marshaller does not support JAXBElement<FlightsType>", marshaller.supports(flightTypeJAXBElement.getClass()));
assertFalse("Jaxb2Marshaller supports class not in context path", marshaller.supports(DummyRootElement.class));
assertFalse("Jaxb2Marshaller supports type not in context path", marshaller.supports((Type)DummyRootElement.class));
method = getClass().getDeclaredMethod("createDummyRootElement");
assertFalse("Jaxb2Marshaller supports JAXBElement not in context path",
marshaller.supports(method.getGenericReturnType()));
assertFalse("Jaxb2Marshaller supports class not in context path", marshaller.supports(DummyType.class));
assertFalse("Jaxb2Marshaller supports type not in context path", marshaller.supports((Type)DummyType.class));
method = getClass().getDeclaredMethod("createDummyType");
assertFalse("Jaxb2Marshaller supports JAXBElement not in context path",
marshaller.supports(method.getGenericReturnType()));
testSupportsPrimitives();
testSupportsStandardClasses();
}
private void testSupportsPrimitives() {
final Primitives primitives = new Primitives();
ReflectionUtils.doWithMethods(Primitives.class, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Type returnType = method.getGenericReturnType();
assertTrue("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(9) + ">",
marshaller.supports(returnType));
try {
// make sure the marshalling does not result in errors
Object returnValue = method.invoke(primitives);
marshaller.marshal(returnValue, new StreamResult(new ByteArrayOutputStream()));
}
catch (InvocationTargetException e) {
fail(e.getMessage());
}
}
}, new ReflectionUtils.MethodFilter() {
@Override
public boolean matches(Method method) {
return method.getName().startsWith("primitive");
}
});
}
private void testSupportsStandardClasses() throws Exception {
final StandardClasses standardClasses = new StandardClasses();
ReflectionUtils.doWithMethods(StandardClasses.class, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Type returnType = method.getGenericReturnType();
assertTrue("Jaxb2Marshaller does not support JAXBElement<" + method.getName().substring(13) + ">",
marshaller.supports(returnType));
try {
// make sure the marshalling does not result in errors
Object returnValue = method.invoke(standardClasses);
marshaller.marshal(returnValue, new StreamResult(new ByteArrayOutputStream()));
}
catch (InvocationTargetException e) {
fail(e.getMessage());
}
}
}, new ReflectionUtils.MethodFilter() {
@Override
public boolean matches(Method method) {
return method.getName().startsWith("standardClass");
}
});
}
@Test
public void supportsXmlRootElement() throws Exception {
marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(DummyRootElement.class, DummyType.class);
marshaller.afterPropertiesSet();
assertTrue("Jaxb2Marshaller does not support XmlRootElement class", marshaller.supports(DummyRootElement.class));
assertTrue("Jaxb2Marshaller does not support XmlRootElement generic type", marshaller.supports((Type)DummyRootElement.class));
assertFalse("Jaxb2Marshaller supports DummyType class", marshaller.supports(DummyType.class));
assertFalse("Jaxb2Marshaller supports DummyType type", marshaller.supports((Type)DummyType.class));
}
@Test
public void marshalAttachments() throws Exception {
marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(BinaryObject.class);
marshaller.setMtomEnabled(true);
marshaller.afterPropertiesSet();
MimeContainer mimeContainer = mock(MimeContainer.class);
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);
assertTrue("No XML written", writer.toString().length() > 0);
verify(mimeContainer, times(3)).addAttachment(isA(String.class), isA(DataHandler.class));
}
@Test
public void marshalAWrappedObjectHoldingAnXmlElementDeclElement() throws Exception {
// SPR-10714
marshaller = new Jaxb2Marshaller();
marshaller.setPackagesToScan(new String[]{"org.springframework.oxm.jaxb"});
marshaller.afterPropertiesSet();
Airplane airplane = new Airplane();
airplane.setName("test");
StringWriter writer = new StringWriter();
Result result = new StreamResult(writer);
marshaller.marshal(airplane, result);
DifferenceEvaluator ev = chain(Default, downgradeDifferencesToEqual(XML_STANDALONE));
assertThat("Marshalling should use root Element",
writer.toString(),
isSimilarTo("<airplane><name>test</name></airplane>").withDifferenceEvaluator(ev));
}
// SPR-10806
@Test
public void unmarshalStreamSourceWithXmlOptions() throws Exception {
final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class);
Jaxb2Marshaller marshaller = new Jaxb2Marshaller() {
@Override
protected javax.xml.bind.Unmarshaller createUnmarshaller() {
return unmarshaller;
}
};
// 1. external-general-entities and dtd support disabled (default)
marshaller.unmarshal(new StreamSource("1"));
ArgumentCaptor<SAXSource> sourceCaptor = ArgumentCaptor.forClass(SAXSource.class);
verify(unmarshaller).unmarshal(sourceCaptor.capture());
SAXSource result = sourceCaptor.getValue();
assertEquals(true, result.getXMLReader().getFeature("http://apache.org/xml/features/disallow-doctype-decl"));
assertEquals(false, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities"));
// 2. external-general-entities and dtd support enabled
reset(unmarshaller);
marshaller.setProcessExternalEntities(true);
marshaller.setSupportDtd(true);
marshaller.unmarshal(new StreamSource("1"));
verify(unmarshaller).unmarshal(sourceCaptor.capture());
result = sourceCaptor.getValue();
assertEquals(false, result.getXMLReader().getFeature("http://apache.org/xml/features/disallow-doctype-decl"));
assertEquals(true, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities"));
}
// SPR-10806
@Test
public void unmarshalSaxSourceWithXmlOptions() throws Exception {
final javax.xml.bind.Unmarshaller unmarshaller = mock(javax.xml.bind.Unmarshaller.class);
Jaxb2Marshaller marshaller = new Jaxb2Marshaller() {
@Override
protected javax.xml.bind.Unmarshaller createUnmarshaller() {
return unmarshaller;
}
};
// 1. external-general-entities and dtd support disabled (default)
marshaller.unmarshal(new SAXSource(new InputSource("1")));
ArgumentCaptor<SAXSource> sourceCaptor = ArgumentCaptor.forClass(SAXSource.class);
verify(unmarshaller).unmarshal(sourceCaptor.capture());
SAXSource result = sourceCaptor.getValue();
assertEquals(true, result.getXMLReader().getFeature("http://apache.org/xml/features/disallow-doctype-decl"));
assertEquals(false, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities"));
// 2. external-general-entities and dtd support enabled
reset(unmarshaller);
marshaller.setProcessExternalEntities(true);
marshaller.setSupportDtd(true);
marshaller.unmarshal(new SAXSource(new InputSource("1")));
verify(unmarshaller).unmarshal(sourceCaptor.capture());
result = sourceCaptor.getValue();
assertEquals(false, result.getXMLReader().getFeature("http://apache.org/xml/features/disallow-doctype-decl"));
assertEquals(true, result.getXMLReader().getFeature("http://xml.org/sax/features/external-general-entities"));
}
@XmlRootElement
@SuppressWarnings("unused")
public static class DummyRootElement {
private DummyType t = new DummyType();
}
@XmlType
@SuppressWarnings("unused")
public static class DummyType {
private String s = "Hello";
}
@SuppressWarnings("unused")
private JAXBElement<DummyRootElement> createDummyRootElement() {
return null;
}
@SuppressWarnings("unused")
private JAXBElement<DummyType> createDummyType() {
return null;
}
}