<url-pattern>/*</url-pattern>
</filter-mapping>
+ <!--
+ A filter that converts XML responses to JSON if needed.
+ -->
+ <filter>
+ <filter-name>XmlToJsonFilter</filter-name>
+ <filter-class>org.collectionspace.services.common.xmljson.XmlToJsonFilter</filter-class>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>XmlToJsonFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
<!--
***
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>org.easymock</groupId>
+ <artifactId>easymock</artifactId>
+ <version>3.4</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
--- /dev/null
+package org.collectionspace.services.common.xmljson;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jboss.resteasy.util.MediaTypeHelper;
+
+/**
+ * Utility methods for working with requests when doing
+ * XML/JSON conversion.
+ */
+public class RequestUtils {
+ /**
+ * Determines if a request's preferred response content
+ * type is JSON.
+ *
+ * @param request the request
+ * @return true if JSON is preferred, false otherwise
+ */
+ public static boolean isJsonPreferred(HttpServletRequest request) {
+ return isJsonPreferred(getAccept(request));
+ }
+
+ /**
+ * Determines if an HTTP Accept header's preferred content
+ * type is JSON.
+ *
+ * @param accept the Accept header value
+ * @return true if JSON is preferred, false otherwise
+ */
+ public static boolean isJsonPreferred(String accept) {
+ if (StringUtils.isBlank(accept)) {
+ return false;
+ }
+
+ List<MediaType> mediaTypes = MediaTypeHelper.parseHeader(accept);
+
+ if (mediaTypes.size() == 0) {
+ return false;
+ }
+
+ MediaTypeHelper.sortByWeight(mediaTypes);
+
+ return MediaTypeHelper.equivalent(mediaTypes.get(0), MediaType.APPLICATION_JSON_TYPE);
+ }
+
+ /**
+ * Constructs an Accept header value from the Accept header
+ * value in the given request, ensuring that XML will be
+ * accepted. If the request already accepts XML, the Accept
+ * value is returned unchanged. If the request does not
+ * accept XML, XML is appended to the Accept value, with the
+ * lowest possible quality factor.
+ *
+ * @param request the request
+ * @return the possibly modified header value, which is
+ * assured to accept XML
+ */
+ public static String getXmlEnsuredAccept(HttpServletRequest request) {
+ return getXmlEnsuredAccept(getAccept(request));
+ }
+
+ /**
+ * Constructs an Accept header value from a base value,
+ * ensuring that XML will be accepted. If the base value
+ * already accepts XML, the base value is returned unchanged.
+ * If the base value does not accept XML, XML is appended to
+ * the value, with the lowest possible quality factor.
+ *
+ * @param accept the base Accept header value
+ * @return the possibly modified header value, which is
+ * assured to accept XML
+ */
+ public static String getXmlEnsuredAccept(String accept) {
+ if (accept.contains(MediaType.APPLICATION_XML)) {
+ return accept;
+ }
+
+ return (accept + "," + MediaType.APPLICATION_XML + ";q=0.001");
+ }
+
+ /**
+ * Returns the value of the HTTP Accept header(s) for a given request.
+ * If multiple Accept headers are present, their values are combined into
+ * one value by joining with a comma.
+ *
+ * @param request the request
+ * @return the (possibly combined) value of the Accept header(s)
+ */
+ public static String getAccept(HttpServletRequest request) {
+ List<?> headers = Collections.list(request.getHeaders(HttpHeaders.ACCEPT));
+ List<String> strings = new ArrayList<String>(headers.size());
+
+ for (Object header : headers) {
+ strings.add(header.toString());
+ }
+
+ return StringUtils.join(strings, ",");
+ }
+}
--- /dev/null
+package org.collectionspace.services.common.xmljson;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.xml.stream.XMLStreamException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+
+/**
+ * A filter that translates XML responses to JSON.
+ *
+ * This filter only has an effect if the preferred content type of the response
+ * (determined from the Accept header) is JSON. If JSON is preferred, both the
+ * request and response are wrapped.
+ *
+ * The request wrapper modifies the Accept header, ensuring that XML is accepted
+ * in addition to (but at a lower quality factor than) JSON. This handles the
+ * case where the original request only accepts JSON. In that case, XML should
+ * also be accepted, so that the XML response may be translated to JSON on the
+ * way back.
+ *
+ * The response wrapper provides a buffered output stream, so that XML output
+ * is captured before being sent over the network. If the content type of the
+ * response is XML, the content type is changed to JSON, the content of the
+ * buffer is translated to JSON, and the JSON is written to the original output
+ * stream. If the content type of the response is not XML, the content type is
+ * not changed, and the content of the buffer is written to the original
+ * output stream unchanged.
+ */
+public class XmlToJsonFilter implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ if (RequestUtils.isJsonPreferred((HttpServletRequest) request)) {
+ // The request prefers a JSON response. Wrap the request and response.
+
+ RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request);
+ ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response);
+
+ chain.doFilter(requestWrapper, responseWrapper);
+
+ if (responseWrapper.getContentType().equals(MediaType.APPLICATION_XML)) {
+ // Got an XML response. Translate it to JSON.
+
+ response.setContentType(MediaType.APPLICATION_JSON);
+
+ try {
+ InputStream xmlInputStream = responseWrapper.getBuffer().toInputStream();
+
+ if (xmlInputStream != null) {
+ OutputStream jsonOutputStream = response.getOutputStream();
+ XmlToJsonStreamConverter converter = new XmlToJsonStreamConverter(xmlInputStream, jsonOutputStream);
+
+ converter.convert();
+ }
+ }
+ catch (XMLStreamException e) {
+ throw new WebApplicationException("Error generating JSON", e);
+ }
+ }
+ else {
+ // Didn't get an XML response. Just pass it along.
+
+ InputStream inputStream = responseWrapper.getBuffer().toInputStream();
+
+ IOUtils.copy(inputStream, response.getOutputStream());
+ }
+ }
+ else {
+ // The request doesn't prefer a JSON response. Just pass it along.
+
+ chain.doFilter(request, response);
+ }
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ /**
+ * A request wrapper that enhances the original Accept header so that
+ * it accepts XML (if it did not already accept XML).
+ */
+ public class RequestWrapper extends HttpServletRequestWrapper {
+ private String xmlEnsuredAccept;
+
+ public RequestWrapper(HttpServletRequest request) {
+ super(request);
+
+ xmlEnsuredAccept = RequestUtils.getXmlEnsuredAccept(request);
+ }
+
+ @Override
+ public Enumeration getHeaders(String name) {
+ if (name.compareToIgnoreCase(HttpHeaders.ACCEPT) == 0) {
+ return Collections.enumeration(Arrays.asList(xmlEnsuredAccept));
+ }
+
+ return super.getHeaders(name);
+ }
+ }
+
+ /**
+ * A response wrapper that replaces the wrapped output stream with a buffered
+ * stream so that output is captured.
+ */
+ public class ResponseWrapper extends HttpServletResponseWrapper {
+ private BufferedServletOutputStream outputStream;
+ private PrintWriter writer;
+
+ public ResponseWrapper(HttpServletResponse response) {
+ super(response);
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ if (writer != null) {
+ throw new IllegalStateException("getWriter has already been called on this response");
+ }
+
+ if (outputStream == null) {
+ outputStream = new BufferedServletOutputStream();
+ }
+
+ return outputStream;
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException {
+ if (outputStream != null && writer == null) {
+ throw new IllegalStateException("getOutputStream has already been called on this response");
+ }
+
+ if (outputStream == null) {
+ outputStream = new BufferedServletOutputStream();
+ }
+
+ if (writer == null) {
+ writer = new PrintWriter(outputStream);
+ }
+
+ return writer;
+ }
+
+ /**
+ * Returns the internal buffer stream.
+ *
+ * @return the buffer stream
+ */
+ public ByteArrayOutputStream getBuffer() {
+ if (outputStream == null) {
+ return null;
+ }
+
+ return outputStream.getBuffer();
+ }
+ }
+
+ /**
+ * A ServletOutputStream that wraps a ByteArrayOutputStream.
+ * Any bytes written are sent to the ByteArrayOutputStream,
+ * which acts as a buffer.
+ */
+ public class BufferedServletOutputStream extends ServletOutputStream {
+ /*
+ * The buffer.
+ *
+ * ByteArrayOutputStream from commons-io provides better
+ * performance than the one from java.io.
+ */
+ private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ @Override
+ public void write(int b) throws IOException {
+ buffer.write(b);
+ }
+
+ /**
+ * Returns the buffer stream.
+ *
+ * @return the buffer stream
+ */
+ public ByteArrayOutputStream getBuffer() {
+ return buffer;
+ }
+ }
+}
--- /dev/null
+package org.collectionspace.services.common.xmljson;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+
+public class XmlToJsonStreamConverter {
+ protected XMLEventReader eventReader;
+ protected PrintWriter writer;
+
+ public XmlToJsonStreamConverter(InputStream in, OutputStream out) throws XMLStreamException {
+ XMLInputFactory factory = XMLInputFactory.newInstance();
+
+ this.eventReader = factory.createXMLEventReader(in);
+ this.writer = new PrintWriter(out);
+ }
+
+ public void convert() throws XMLStreamException {
+ writer.print("{\"foo\": \"bar\"}");
+ writer.flush();
+// while(eventReader.hasNext()){
+// XMLEvent event = eventReader.nextEvent();
+//
+// switch(event.getEventType()) {
+//
+// }
+// }
+ }
+}
--- /dev/null
+package org.collectionspace.services.common.xmljson.test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static org.collectionspace.services.common.xmljson.RequestUtils.*;
+import static org.testng.Assert.*;
+
+import org.easymock.EasyMock;
+import org.testng.annotations.Test;
+
+public class RequestUtilsTest {
+
+ @Test
+ public void testIsJsonPreferred() {
+ assertEquals(
+ isJsonPreferred("application/json"),
+ true);
+ assertEquals(
+ isJsonPreferred("application/xml"),
+ false);
+ assertEquals(
+ isJsonPreferred("*/*"),
+ false);
+ assertEquals(
+ isJsonPreferred(""),
+ false);
+ assertEquals(
+ isJsonPreferred((String) null),
+ false);
+ assertEquals(
+ isJsonPreferred("application/json,application/xml;q=0.9"),
+ true);
+ assertEquals(
+ isJsonPreferred("application/json;q=0.8,application/xml;q=0.9"),
+ false);
+ assertEquals(
+ isJsonPreferred("application/json;q=0.8,application/xml;q=0.7"),
+ true);
+ }
+
+ @Test
+ public void testGetXmlEnsuredAccept() {
+ assertEquals(
+ getXmlEnsuredAccept("application/json"),
+ "application/json,application/xml;q=0.001");
+ assertEquals(
+ getXmlEnsuredAccept("application/xml"),
+ "application/xml");
+ assertEquals(
+ getXmlEnsuredAccept("application/json,application/xml;q=0.9"),
+ "application/json,application/xml;q=0.9");
+ assertEquals(
+ getXmlEnsuredAccept("application/json;q=0.8,application/xml;q=0.9"),
+ "application/json;q=0.8,application/xml;q=0.9");
+ assertEquals(
+ getXmlEnsuredAccept("application/json;q=0.8,application/xml;q=0.7"),
+ "application/json;q=0.8,application/xml;q=0.7");
+ assertEquals(
+ getXmlEnsuredAccept("application/xml;q=0.7,application/json;q=0.8"),
+ "application/xml;q=0.7,application/json;q=0.8");
+ }
+
+ @Test
+ public void testGetAccept() {
+ assertEquals(
+ getAccept(requestAccepting("application/json")),
+ "application/json");
+ assertEquals(
+ getAccept(requestAccepting("application/xml")),
+ "application/xml");
+ assertEquals(
+ getAccept(requestAccepting("application/json,application/xml")),
+ "application/json,application/xml");
+ assertEquals(
+ getAccept(requestAccepting("application/json", "application/xml")),
+ "application/json,application/xml");
+ assertEquals(
+ getAccept(requestAccepting("*/*", "application/xml;q=0.9", "application/json;q=0.4")),
+ "*/*,application/xml;q=0.9,application/json;q=0.4");
+ }
+
+ private HttpServletRequest requestAccepting(String... accepts) {
+ HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+
+ EasyMock.expect(request.getHeaders("Accept"))
+ .andReturn(Collections.enumeration(Arrays.asList(accepts)));
+
+ EasyMock.replay(request);
+
+ return request;
+ }
+}
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
- <version>2.0.1</version>
+ <version>2.5</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>