]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
DRYD-23: Implement xml to json servlet filter.
authorRay Lee <rhlee@berkeley.edu>
Thu, 28 Jul 2016 00:04:48 +0000 (17:04 -0700)
committerRay Lee <rhlee@berkeley.edu>
Wed, 3 Aug 2016 18:09:45 +0000 (11:09 -0700)
services/JaxRsServiceProvider/src/main/webapp/WEB-INF/web.xml
services/common/pom.xml
services/common/src/main/java/org/collectionspace/services/common/xmljson/RequestUtils.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlToJsonFilter.java [new file with mode: 0644]
services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlToJsonStreamConverter.java [new file with mode: 0644]
services/common/src/test/java/org/collectionspace/services/common/xmljson/test/RequestUtilsTest.java [new file with mode: 0644]
services/pom.xml

index 0aaf52115103a84bd564093c0a73537fd4f16326..c375f1830926e5c731cdb1d5a6aeeb00b39d8c17 100644 (file)
                <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>
 
     <!--
        ***
index c66492458fe6864352bfae2f6b53ba97d100c5fc..c6f436e6399fd64960b2565cda6d7825a5ee0783 100644 (file)
                        <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>
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/RequestUtils.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/RequestUtils.java
new file mode 100644 (file)
index 0000000..850e6a2
--- /dev/null
@@ -0,0 +1,106 @@
+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, ",");
+    }
+}
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlToJsonFilter.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlToJsonFilter.java
new file mode 100644 (file)
index 0000000..87a221b
--- /dev/null
@@ -0,0 +1,214 @@
+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;
+        }
+    }
+}
diff --git a/services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlToJsonStreamConverter.java b/services/common/src/main/java/org/collectionspace/services/common/xmljson/XmlToJsonStreamConverter.java
new file mode 100644 (file)
index 0000000..6963533
--- /dev/null
@@ -0,0 +1,33 @@
+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()) {
+//            
+//            }
+//        }
+    }
+}
diff --git a/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/RequestUtilsTest.java b/services/common/src/test/java/org/collectionspace/services/common/xmljson/test/RequestUtilsTest.java
new file mode 100644 (file)
index 0000000..1d0db32
--- /dev/null
@@ -0,0 +1,95 @@
+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;
+    }
+}
index 9d3edee8d78db6456c2eb810d6819177d5328adf..7c9b89872764348e3fab3903d93fedca666c3d8f 100644 (file)
             <dependency>
                 <groupId>commons-io</groupId>
                 <artifactId>commons-io</artifactId>
-                               <version>2.0.1</version>
+                <version>2.5</version>
             </dependency>
             <dependency>
                 <groupId>commons-cli</groupId>