2

QUESTION:
Is it possible to extract the original message body string (i.e., XML string or JSON string) - within the "post()" method - of a REST service?

Environment

Java 8

WebLogic 12.1.3 (w/ jax-rs(2.0,2.5.1) deployable library)

(The "request.getInputStream()" yields nothing... Seems that "read()" has already been applied "internally". Also, "mark()" or "reset()" is not supported)

"post()" method...

    package aaa.bbb.ccc;

    import javax.ejb.Stateless;
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    import aaa.bbb.ccc.fieldslist.FieldsList;
    import java.io.*;
    import java.net.URI;
    import javax.ws.rs.core.*;
    import javax.xml.bind.JAXBException;
    import org.apache.commons.io.IOUtils;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;

    @Stateless
    @Path("/fieldsList")
    public class Testws {

        private static final Logger LOG = LogManager.getLogger(Testws.class);

        public Testws() {
        }

        @POST
        @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
        public Response post(@Context UriInfo uriInfo, @Context javax.servlet.http.HttpServletRequest request, FieldsList fieldsList) throws IOException, JAXBException, Exception {
            try {

                //...returns empty string...
                String s = IOUtils.toString(request.getInputStream(), "UTF-8");

                LOG.info("message string from request.getInputStream()=" + s); <==  empty string...
            } catch (Exception e) {
                e.printStackTrace();
            }

            URI uri = UriBuilder.fromUri(uriInfo.getRequestUri()).build();

            Response response = Response.created(uri).build();
            return response;
        }
    }

I've tried using an interceptor (see "aroundReadFrom()" method) to manipulate the InputStream before it is used by the post() method, but, to no effect... -That is, in the REST service's post() method, the request.getInputStream() continues to yield nothing...

"aroundReadFrom()" method...

    package aaa.bbb.ccc;

    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import javax.ws.rs.WebApplicationException;
    import javax.ws.rs.ext.Provider;
    import javax.ws.rs.ext.ReaderInterceptor;
    import javax.ws.rs.ext.ReaderInterceptorContext;
    import org.apache.commons.io.IOUtils;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;

    @Provider
    public class MyReaderInterceptor implements ReaderInterceptor {

        static final Logger LOG = LogManager.getLogger(MyReaderInterceptor.class);

        @Override
        public Object aroundReadFrom(ReaderInterceptorContext ctx) throws IOException {

            try {
                InputStream is = ctx.getInputStream();
                byte[] content = IOUtils.toByteArray(is);
                is.close();

                ctx.setInputStream(new ByteArrayInputStream(content));            

                return ctx.proceed();            
            } catch (IOException | WebApplicationException e) {
                e.printStackTrace();
            }

            return null;
        }
    }

Here is the test xml message...:

    <?xml version="1.0" encoding="UTF-8"?>
    <FieldsList xmlns="http://aaa.bbb.ccc.ws/testws">
        <Fields>
            <FieldA>fieldA_value</FieldA>
            <FieldB>fieldB_value</FieldB>
            <FieldC>fieldC_value</FieldC>
        </Fields>
    </FieldsList>

Here is the schema:

    <?xml version="1.0" encoding="UTF-8"?>
    <xs:schema 
        targetNamespace="http://aaa.bbb.ccc.ws/testws"
        attributeFormDefault="unqualified"
        elementFormDefault="qualified"
        xmlns:tw="http://aaa.bbb.ccc.ws/testws"
        xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <xs:complexType name="FieldsType">
            <xs:all>
                <xs:element name="FieldA" type="xs:string" minOccurs="0" />            
                <xs:element name="FieldB" type="xs:string" minOccurs="0" />            
                <xs:element name="FieldC" type="xs:string" minOccurs="0" />                                                 
            </xs:all>
        </xs:complexType>
        <xs:element name="FieldsList">
            <xs:complexType>
                <xs:sequence>
                    <xs:element name="Fields" type="tw:FieldsType" minOccurs="0" maxOccurs="unbounded" />
                </xs:sequence>
            </xs:complexType>
        </xs:element>
    </xs:schema>

UPDATE:

Within the post() method I've only been able to reconstruct message string using this technique...

    StringWriter sw = new StringWriter();
    JAXBContext.newInstance(FieldsList.class).createMarshaller().marshal(fieldsList, sw);
    System.out.println("posted xml string=" + sw.toString());

...However, this would not help if the same data is posted in JSON format. To clarify, it will reconstruct the JSON post message as an XML string rather than the original JSON string

Again, I what I'm trying to do is access the original posted XML/JSON message string within the post() method

sairn
  • 461
  • 3
  • 24
  • 58
  • Remove bais.reset(); should work! – LHA Jan 27 '17 at 16:17
  • Hi Loc - I can certainly obtain the original message string inside the "aroundReadFrom" method of the interceptor. -However, inside the "post()" method, the "request.getInputStream()" will still not yield the the original message string... -That is, calling 'IOUtils.toString(request.getInputStream(), "UTF-8")' returns only an empty string... -Also, as I indicated, the "mark()" and "reset()" methods are unavailable to the ServletInputStream object that is returned by calling "request.getInputStream()" in the "post()" method. Therefore, I cannot manipulate the pointer. – sairn Jan 27 '17 at 19:32

2 Answers2

1

You can just use a ReaderInterceptor. This is where you can get the raw data. You basically need to get the InputStream from the ReaderInterceptorContext, read it, then you need to set the new InputStream, since the original InputStream can only be read once. So you need to use some buffering strategy when reading the original stream

@Override
public Object aroundReadFrom(ReaderInterceptorContext context) {
    InputStream ogStream = context.getInputStream();
    // readStream with some buffer
    // set new stream
    context.setInputStream(bufferedStream);
    return context.proceed();
}

See Also:

Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Thank you, peeskillet. What I am looking for is a way to extract the original XML/JSON string that was posted whenever an Exception occurs in the REST service. In otherwords, I want to log the posted request body (XML or JSON string) in try-catch block (in the post() method) when an exception occurs (if this makes sense). Unfortunately, request.getInputStream() is "empty" at this point. Thanks again! – sairn Jan 18 '17 at 22:53
1

Solution using Request Attribute - Tested work 100%

@Provider
public class ContainerRequestFilterImpl implements ContainerRequestFilter {

    @Context
    private HttpServletRequest request;

    @Override
    public void filter(ContainerRequestContext ctx) throws IOException {

        InputStream is = ctx.getEntityStream();
        byte[] content = IOUtils.toByteArray(is);

        // Store content as Request Attribute
        request.setAttribute("content", new String(content, "UTF-8"));

        ctx.setEntityStream(new ByteArrayInputStream(content));
    }
}


AND

@POST
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response post(@Context UriInfo uriInfo, @Context HttpServletRequest request, FieldsList fieldsList) throws IOException, JAXBException, Exception {
    try {
        String s = request.getAttribute("postContent");
        LOG.info("Post content: " + s);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

I think you can use ReaderInterceptor instead of ContainerRequestFilter. It should work too.

LHA
  • 9,398
  • 8
  • 46
  • 85
  • Hi Loc, thx for your efforts. I already knew about this alternative. However, I trying to derive/extract the original message string from the InputStream and not duplicate/double the size of the request object by making a copy in a request attribute. I'll be dealing with very large messages. NOTE: If it turns out that this is NOT possible, then that is a valid "answer", too. Just want some supporting documentation references accompanying that answer. thx! – sairn Jan 30 '17 at 14:38
  • @sairm: "copy in a request attribute"? There is NO copy of request attribute. They are referencing to the same Content object memory. Content string memory just created only once here. – LHA Jan 30 '17 at 14:47
  • Ah!... -It seemed a new "byte[]" object was being created, which was populated using the "request.getEntityStream()". And, then a new "String()" object was created using the content of the "byte[]" object. And, then, the new "String()" object is stored in the request object's attribute map. You sure there are no new bytes (i.e., the size of the string) added to the request object? I just want to be sure I understand. thx! – sairn Jan 30 '17 at 16:07
  • "You sure there are no new bytes (i.e., the size of the string) added to the request object?" --- YES. You got confused between object memory allocation and object reference. A reference does not cause a new memory allocation, it just point to a existing object memory. – LHA Jan 30 '17 at 16:49
  • 1
    K, thanks, Loc! I had rejected this solution because I thought that adding the message string to the request object's attribute map would increase the request object size. Clearly I was wrong! :-) -I'll check your answer. thx! – sairn Jan 31 '17 at 16:07
  • 1
    Loc - do you have a way of "proving" this concept? I had an idea of measuring the request object's byte-length (i.e., before and after) to comparison. However, the request object is NOT serializable. -Any ideas?? – sairn Jan 31 '17 at 21:02
  • 1
    Not the best explanation, really. :-) – sairn Feb 01 '17 at 22:54
  • When you convert a stream to byte array you must do a copy. So yes, the request size will get bigger. Just check IOUtils.toByteArray(is) and you will see there is a copy method inside. – ceklock May 04 '22 at 15:41
  • 1
    @ceklock Dont try to be smarter than me. I knew what I did. It was only the solution. There is no solution without copying the input stream to the byte array. – LHA May 04 '22 at 16:03