/*
* Copyright 2002-2016 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.http.converter.xml;
import static org.junit.Assert.*;
import static org.xmlunit.diff.ComparisonType.*;
import static org.xmlunit.diff.DifferenceEvaluators.*;
import static org.xmlunit.matchers.CompareMatcher.*;
import java.nio.charset.StandardCharsets;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.xmlunit.diff.DifferenceEvaluator;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.framework.DefaultAopProxyFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
/**
* Tests for {@link Jaxb2RootElementHttpMessageConverter}.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
*/
public class Jaxb2RootElementHttpMessageConverterTests {
private Jaxb2RootElementHttpMessageConverter converter;
private RootElement rootElement;
private RootElement rootElementCglib;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
converter = new Jaxb2RootElementHttpMessageConverter();
rootElement = new RootElement();
DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory();
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(rootElement);
advisedSupport.setProxyTargetClass(true);
AopProxy proxy = proxyFactory.createAopProxy(advisedSupport);
rootElementCglib = (RootElement) proxy.getProxy();
}
@Test
public void canRead() throws Exception {
assertTrue("Converter does not support reading @XmlRootElement", converter.canRead(RootElement.class, null));
assertTrue("Converter does not support reading @XmlType", converter.canRead(Type.class, null));
}
@Test
public void canWrite() throws Exception {
assertTrue("Converter does not support writing @XmlRootElement", converter.canWrite(RootElement.class, null));
assertTrue("Converter does not support writing @XmlRootElement subclass", converter.canWrite(RootElementSubclass.class, null));
assertTrue("Converter does not support writing @XmlRootElement subclass", converter.canWrite(rootElementCglib.getClass(), null));
assertFalse("Converter supports writing @XmlType", converter.canWrite(Type.class, null));
}
@Test
public void readXmlRootElement() throws Exception {
byte[] body = "<rootElement><type s=\"Hello World\"/></rootElement>".getBytes("UTF-8");
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
RootElement result = (RootElement) converter.read(RootElement.class, inputMessage);
assertEquals("Invalid result", "Hello World", result.type.s);
}
@Test
public void readXmlRootElementSubclass() throws Exception {
byte[] body = "<rootElement><type s=\"Hello World\"/></rootElement>".getBytes("UTF-8");
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
RootElementSubclass result = (RootElementSubclass) converter.read(RootElementSubclass.class, inputMessage);
assertEquals("Invalid result", "Hello World", result.getType().s);
}
@Test
public void readXmlType() throws Exception {
byte[] body = "<foo s=\"Hello World\"/>".getBytes("UTF-8");
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
Type result = (Type) converter.read(Type.class, inputMessage);
assertEquals("Invalid result", "Hello World", result.s);
}
@Test
public void readXmlRootElementExternalEntityDisabled() throws Exception {
Resource external = new ClassPathResource("external.txt", getClass());
String content = "<!DOCTYPE root SYSTEM \"http://192.168.28.42/1.jsp\" [" +
" <!ELEMENT external ANY >\n" +
" <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" +
" <rootElement><external>&ext;</external></rootElement>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
converter.setSupportDtd(true);
RootElement rootElement = (RootElement) converter.read(RootElement.class, inputMessage);
assertEquals("", rootElement.external);
}
@Test
public void readXmlRootElementExternalEntityEnabled() throws Exception {
Resource external = new ClassPathResource("external.txt", getClass());
String content = "<!DOCTYPE root [" +
" <!ELEMENT external ANY >\n" +
" <!ENTITY ext SYSTEM \"" + external.getURI() + "\" >]>" +
" <rootElement><external>&ext;</external></rootElement>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
this.converter.setProcessExternalEntities(true);
RootElement rootElement = (RootElement) converter.read(RootElement.class, inputMessage);
assertEquals("Foo Bar", rootElement.external);
}
@Test
public void testXmlBomb() throws Exception {
// https://en.wikipedia.org/wiki/Billion_laughs
// https://msdn.microsoft.com/en-us/magazine/ee335713.aspx
String content = "<?xml version=\"1.0\"?>\n" +
"<!DOCTYPE lolz [\n" +
" <!ENTITY lol \"lol\">\n" +
" <!ELEMENT lolz (#PCDATA)>\n" +
" <!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" +
" <!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n" +
" <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n" +
" <!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n" +
" <!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">\n" +
" <!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n" +
" <!ENTITY lol7 \"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;\">\n" +
" <!ENTITY lol8 \"&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;\">\n" +
" <!ENTITY lol9 \"&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;\">\n" +
"]>\n" +
"<rootElement><external>&lol9;</external></rootElement>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
this.thrown.expect(HttpMessageNotReadableException.class);
this.thrown.expectMessage("DOCTYPE");
this.converter.read(RootElement.class, inputMessage);
}
@Test
public void writeXmlRootElement() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(rootElement, null, outputMessage);
assertEquals("Invalid content-type", new MediaType("application", "xml"),
outputMessage.getHeaders().getContentType());
DifferenceEvaluator ev = chain(Default, downgradeDifferencesToEqual(XML_STANDALONE));
assertThat("Invalid result", outputMessage.getBodyAsString(StandardCharsets.UTF_8),
isSimilarTo("<rootElement><type s=\"Hello World\"/></rootElement>").withDifferenceEvaluator(ev));
}
@Test
public void writeXmlRootElementSubclass() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(rootElementCglib, null, outputMessage);
assertEquals("Invalid content-type", new MediaType("application", "xml"),
outputMessage.getHeaders().getContentType());
DifferenceEvaluator ev = chain(Default, downgradeDifferencesToEqual(XML_STANDALONE));
assertThat("Invalid result", outputMessage.getBodyAsString(StandardCharsets.UTF_8),
isSimilarTo("<rootElement><type s=\"Hello World\"/></rootElement>").withDifferenceEvaluator(ev));
}
// SPR-11488
@Test
public void customizeMarshaller() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
MyJaxb2RootElementHttpMessageConverter myConverter = new MyJaxb2RootElementHttpMessageConverter();
myConverter.write(new MyRootElement(new MyCustomElement("a", "b")), null, outputMessage);
DifferenceEvaluator ev = chain(Default, downgradeDifferencesToEqual(XML_STANDALONE));
assertThat("Invalid result", outputMessage.getBodyAsString(StandardCharsets.UTF_8),
isSimilarTo("<myRootElement><element>a|||b</element></myRootElement>").withDifferenceEvaluator(ev));
}
@Test
public void customizeUnmarshaller() throws Exception {
byte[] body = "<myRootElement><element>a|||b</element></myRootElement>".getBytes("UTF-8");
MyJaxb2RootElementHttpMessageConverter myConverter = new MyJaxb2RootElementHttpMessageConverter();
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body);
MyRootElement result = (MyRootElement) myConverter.read(MyRootElement.class, inputMessage);
assertEquals("a", result.getElement().getField1());
assertEquals("b", result.getElement().getField2());
}
@XmlRootElement
public static class RootElement {
private Type type = new Type();
@XmlElement(required=false)
public String external;
public Type getType() {
return this.type;
}
@XmlElement
public void setType(Type type) {
this.type = type;
}
}
@XmlType
public static class Type {
@XmlAttribute
public String s = "Hello World";
}
public static class RootElementSubclass extends RootElement {
}
public static class MyJaxb2RootElementHttpMessageConverter extends Jaxb2RootElementHttpMessageConverter {
@Override
protected void customizeMarshaller(Marshaller marshaller) {
marshaller.setAdapter(new MyCustomElementAdapter());
}
@Override
protected void customizeUnmarshaller(Unmarshaller unmarshaller) {
unmarshaller.setAdapter(new MyCustomElementAdapter());
}
}
public static class MyCustomElement {
private String field1;
private String field2;
public MyCustomElement() {
}
public MyCustomElement(String field1, String field2) {
this.field1 = field1;
this.field2 = field2;
}
public String getField1() {
return field1;
}
public void setField1(String field1) {
this.field1 = field1;
}
public String getField2() {
return field2;
}
public void setField2(String field2) {
this.field2 = field2;
}
}
@XmlRootElement
public static class MyRootElement {
private MyCustomElement element;
public MyRootElement() {
}
public MyRootElement(MyCustomElement element) {
this.element = element;
}
@XmlJavaTypeAdapter(MyCustomElementAdapter.class)
public MyCustomElement getElement() {
return element;
}
public void setElement(MyCustomElement element) {
this.element = element;
}
}
public static class MyCustomElementAdapter extends XmlAdapter<String, MyCustomElement> {
@Override
public String marshal(MyCustomElement c) throws Exception {
return c.getField1() + "|||" + c.getField2();
}
@Override
public MyCustomElement unmarshal(String c) throws Exception {
String[] t = c.split("\\|\\|\\|");
return new MyCustomElement(t[0], t[1]);
}
}
}