/*
* 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.xstream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.extended.EncodedByteArrayConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
import com.thoughtworks.xstream.io.json.JsonWriter;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.InOrder;
import org.springframework.util.xml.StaxUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xmlunit.builder.Input;
import org.xmlunit.xpath.JAXPXPathEngine;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
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.sax.SAXResult;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayOutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.eq;
import static org.mockito.BDDMockito.inOrder;
import static org.mockito.BDDMockito.isA;
import static org.mockito.BDDMockito.mock;
import static org.xmlunit.matchers.CompareMatcher.isSimilarTo;
/**
* @author Arjen Poutsma
* @author Sam Brannen
*/
public class XStreamMarshallerTests {
private static final String EXPECTED_STRING = "<flight><flightNumber>42</flightNumber></flight>";
private XStreamMarshaller marshaller;
private Flight flight;
@Before
public void createMarshaller() throws Exception {
marshaller = new XStreamMarshaller();
Map<String, String> aliases = new HashMap<>();
aliases.put("flight", Flight.class.getName());
marshaller.setAliases(aliases);
flight = new Flight();
flight.setFlightNumber(42L);
}
@Test
public void marshalDOMResult() throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
Document document = builder.newDocument();
DOMResult domResult = new DOMResult(document);
marshaller.marshal(flight, domResult);
Document expected = builder.newDocument();
Element flightElement = expected.createElement("flight");
expected.appendChild(flightElement);
Element numberElement = expected.createElement("flightNumber");
flightElement.appendChild(numberElement);
Text text = expected.createTextNode("42");
numberElement.appendChild(text);
assertThat("Marshaller writes invalid DOMResult", document, isSimilarTo(expected));
}
// see SWS-392
@Test
public void marshalDOMResultToExistentDocument() throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
Document existent = builder.newDocument();
Element rootElement = existent.createElement("root");
Element flightsElement = existent.createElement("flights");
rootElement.appendChild(flightsElement);
existent.appendChild(rootElement);
// marshall into the existent document
DOMResult domResult = new DOMResult(flightsElement);
marshaller.marshal(flight, domResult);
Document expected = builder.newDocument();
Element eRootElement = expected.createElement("root");
Element eFlightsElement = expected.createElement("flights");
Element eFlightElement = expected.createElement("flight");
eRootElement.appendChild(eFlightsElement);
eFlightsElement.appendChild(eFlightElement);
expected.appendChild(eRootElement);
Element eNumberElement = expected.createElement("flightNumber");
eFlightElement.appendChild(eNumberElement);
Text text = expected.createTextNode("42");
eNumberElement.appendChild(text);
assertThat("Marshaller writes invalid DOMResult", existent, isSimilarTo(expected));
}
@Test
public void marshalStreamResultWriter() throws Exception {
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
marshaller.marshal(flight, result);
assertThat("Marshaller writes invalid StreamResult", writer.toString(), isSimilarTo(EXPECTED_STRING));
}
@Test
public void marshalStreamResultOutputStream() throws Exception {
ByteArrayOutputStream os = new ByteArrayOutputStream();
StreamResult result = new StreamResult(os);
marshaller.marshal(flight, result);
String s = new String(os.toByteArray(), "UTF-8");
assertThat("Marshaller writes invalid StreamResult", s, isSimilarTo(EXPECTED_STRING));
}
@Test
public void marshalSaxResult() throws Exception {
ContentHandler contentHandler = mock(ContentHandler.class);
SAXResult result = new SAXResult(contentHandler);
marshaller.marshal(flight, result);
InOrder ordered = inOrder(contentHandler);
ordered.verify(contentHandler).startDocument();
ordered.verify(contentHandler).startElement(eq(""), eq("flight"), eq("flight"), isA(Attributes.class));
ordered.verify(contentHandler).startElement(eq(""), eq("flightNumber"), eq("flightNumber"), isA(Attributes.class));
ordered.verify(contentHandler).characters(isA(char[].class), eq(0), eq(2));
ordered.verify(contentHandler).endElement("", "flightNumber", "flightNumber");
ordered.verify(contentHandler).endElement("", "flight", "flight");
ordered.verify(contentHandler).endDocument();
}
@Test
public void marshalStaxResultXMLStreamWriter() throws Exception {
XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
StringWriter writer = new StringWriter();
XMLStreamWriter streamWriter = outputFactory.createXMLStreamWriter(writer);
Result result = StaxUtils.createStaxResult(streamWriter);
marshaller.marshal(flight, result);
assertThat("Marshaller writes invalid StreamResult", writer.toString(), isSimilarTo(EXPECTED_STRING));
}
@Test
public void marshalStaxResultXMLEventWriter() throws Exception {
XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
StringWriter writer = new StringWriter();
XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(writer);
Result result = StaxUtils.createStaxResult(eventWriter);
marshaller.marshal(flight, result);
assertThat("Marshaller writes invalid StreamResult", writer.toString(), isSimilarTo(EXPECTED_STRING));
}
@Test
public void converters() throws Exception {
marshaller.setConverters(new Converter[]{new EncodedByteArrayConverter()});
byte[] buf = new byte[]{0x1, 0x2};
Writer writer = new StringWriter();
marshaller.marshal(buf, new StreamResult(writer));
assertThat(writer.toString(), isSimilarTo("<byte-array>AQI=</byte-array>"));
Reader reader = new StringReader(writer.toString());
byte[] bufResult = (byte[]) marshaller.unmarshal(new StreamSource(reader));
assertTrue("Invalid result", Arrays.equals(buf, bufResult));
}
@Test
public void useAttributesFor() throws Exception {
marshaller.setUseAttributeForTypes(new Class[]{Long.TYPE});
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
String expected = "<flight flightNumber=\"42\" />";
assertThat("Marshaller does not use attributes", writer.toString(), isSimilarTo(expected));
}
@Test
public void useAttributesForStringClassMap() throws Exception {
marshaller.setUseAttributeFor(Collections.singletonMap("flightNumber", Long.TYPE));
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
String expected = "<flight flightNumber=\"42\" />";
assertThat("Marshaller does not use attributes", writer.toString(), isSimilarTo(expected));
}
@Test
public void useAttributesForClassStringMap() throws Exception {
marshaller.setUseAttributeFor(Collections.singletonMap(Flight.class, "flightNumber"));
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
String expected = "<flight flightNumber=\"42\" />";
assertThat("Marshaller does not use attributes", writer.toString(), isSimilarTo(expected));
}
@Test
public void useAttributesForClassStringListMap() throws Exception {
marshaller
.setUseAttributeFor(Collections.singletonMap(Flight.class, Collections.singletonList("flightNumber")));
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
String expected = "<flight flightNumber=\"42\" />";
assertThat("Marshaller does not use attributes", writer.toString(), isSimilarTo(expected));
}
@Test
@Ignore("Fails on JDK 8 build 108")
public void aliasesByTypeStringClassMap() throws Exception {
Map<String, Class<?>> aliases = new HashMap<>();
aliases.put("flight", Flight.class);
FlightSubclass flight = new FlightSubclass();
flight.setFlightNumber(42);
marshaller.setAliasesByType(aliases);
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
assertThat("Marshaller does not use attributes", writer.toString(), isSimilarTo(EXPECTED_STRING));
}
@Test
@Ignore("Fails on JDK 8 build 108")
public void aliasesByTypeStringStringMap() throws Exception {
Map<String, String> aliases = new HashMap<>();
aliases.put("flight", Flight.class.getName());
FlightSubclass flight = new FlightSubclass();
flight.setFlightNumber(42);
marshaller.setAliasesByType(aliases);
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
assertThat("Marshaller does not use attributes", writer.toString(), isSimilarTo(EXPECTED_STRING));
}
@Test
public void fieldAliases() throws Exception {
marshaller.setFieldAliases(Collections.singletonMap("org.springframework.oxm.xstream.Flight.flightNumber", "flightNo"));
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
String expected = "<flight><flightNo>42</flightNo></flight>";
assertThat("Marshaller does not use aliases", writer.toString(), isSimilarTo(expected));
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
public void omitFields() throws Exception {
Map omittedFieldsMap = Collections.singletonMap(Flight.class, "flightNumber");
marshaller.setOmittedFields(omittedFieldsMap);
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
assertXpathNotExists("/flight/flightNumber", writer.toString());
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
public void implicitCollections() throws Exception {
Flights flights = new Flights();
flights.getFlights().add(flight);
flights.getStrings().add("42");
Map<String, Class<?>> aliases = new HashMap<>();
aliases.put("flight", Flight.class);
aliases.put("flights", Flights.class);
marshaller.setAliases(aliases);
Map implicitCollections = Collections.singletonMap(Flights.class, "flights,strings");
marshaller.setImplicitCollections(implicitCollections);
Writer writer = new StringWriter();
marshaller.marshal(flights, new StreamResult(writer));
String result = writer.toString();
assertXpathNotExists("/flights/flights", result);
assertXpathExists("/flights/flight", result);
assertXpathNotExists("/flights/strings", result);
assertXpathExists("/flights/string", result);
}
@Test
public void jettisonDriver() throws Exception {
marshaller.setStreamDriver(new JettisonMappedXmlDriver());
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
assertEquals("Invalid result", "{\"flight\":{\"flightNumber\":42}}", writer.toString());
Object o = marshaller.unmarshal(new StreamSource(new StringReader(writer.toString())));
assertTrue("Unmarshalled object is not Flights", o instanceof Flight);
Flight unflight = (Flight) o;
assertNotNull("Flight is null", unflight);
assertEquals("Number is invalid", 42L, unflight.getFlightNumber());
}
@Test
public void jsonDriver() throws Exception {
marshaller.setStreamDriver(new JsonHierarchicalStreamDriver() {
@Override
public HierarchicalStreamWriter createWriter(Writer writer) {
return new JsonWriter(writer, JsonWriter.DROP_ROOT_MODE,
new JsonWriter.Format(new char[0], new char[0],
JsonWriter.Format.SPACE_AFTER_LABEL | JsonWriter.Format.COMPACT_EMPTY_ELEMENT));
}
});
Writer writer = new StringWriter();
marshaller.marshal(flight, new StreamResult(writer));
assertEquals("Invalid result", "{\"flightNumber\": 42}", writer.toString());
}
@Test
public void annotatedMarshalStreamResultWriter() throws Exception {
marshaller.setAnnotatedClasses(Flight.class);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
Flight flight = new Flight();
flight.setFlightNumber(42);
marshaller.marshal(flight, result);
String expected = "<flight><number>42</number></flight>";
assertThat("Marshaller writes invalid StreamResult", writer.toString(), isSimilarTo(expected));
}
private static void assertXpathExists(String xPathExpression, String inXMLString){
Source source = Input.fromString(inXMLString).build();
Iterable<Node> nodes = new JAXPXPathEngine().selectNodes(xPathExpression, source);
assertTrue("Expecting to find matches for Xpath " + xPathExpression, count(nodes) > 0);
}
private static void assertXpathNotExists(String xPathExpression, String inXMLString){
Source source = Input.fromString(inXMLString).build();
Iterable<Node> nodes = new JAXPXPathEngine().selectNodes(xPathExpression, source);
assertEquals("Should be zero matches for Xpath " + xPathExpression, 0, count(nodes));
}
private static int count(Iterable<Node> nodes) {
assertNotNull(nodes);
AtomicInteger count = new AtomicInteger();
nodes.forEach(n -> count.incrementAndGet());
return count.get();
}
}