6

In my web server application I have a method, which modifies an xml document and looks similar to that:

@POST
@Path("somePath")
@Consumes({"application/xml", "application/zip"})
public Response modifyXml() {
    //some processing
}

The consumed zip archive contains the xml file which needs to be modified and some other files. How can I distinguish between consumed xml file and the archive inside the method and which kind of method parameter should I use to represent this consumed resource?

Arthur Eirich
  • 3,368
  • 9
  • 31
  • 63
  • `@param filetype:String`? – Mr. Polywhirl Dec 30 '14 at 15:03
  • @Mr.Polywhirl It definitely makes sense to treat the xml as a string which I am already doing in processing the incoming xml file, but how should I be in case it is a zip archive which contains the xml? – Arthur Eirich Dec 30 '14 at 15:05
  • Does your problem solve question asked here? http://stackoverflow.com/questions/17864158/multiple-get-for-different-mime-how-to-consume-this-with-plain-httpurlconnect – Hegde Dec 30 '14 at 15:06
  • @Hegde Thanks for pointing! I already viewed this question and thought to separate the resources, but I am afraid it really should be done in one method – Arthur Eirich Dec 30 '14 at 15:09

1 Answers1

5

One solution is to just read from an InputStream. You could wrap the InputStream in a ZipInputStream. With ZipInputStream you can get a ZipEntry with ZipInputStream.getNextEntry(), then you can get the file name with ZipEntry.getName(). Then just check if the name endsWith(".xml").

With this though, you would need to consume application/octet-stream. Here's a simple example

@Path("/zip")
public class ZipResource {

    @POST
    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
    public Response postZipFile(InputStream is) throws Exception {
        StringBuilder builder;
        try (ZipInputStream zip = new ZipInputStream(is)) {
            builder = new StringBuilder("==== Data ====\n");
            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                String filename = entry.getName();
                if (filename.endsWith(".xml")) {
                    builder.append("name: ").append(entry.getName()).append("\n");
                    String xml = filePartToString(zip, (int) entry.getSize());
                    builder.append("data: ").append(xml).append("\n");
                }
                zip.closeEntry();
            }
        }
        return Response.ok(builder.toString()).build();
    }

    private String filePartToString(InputStream is, int size) throws Exception {
        String xml;
        byte[] buff = new byte[size];
        is.read(buff, 0, size);
        return new String(buff);
    }
}

Here's a test

@Test
public void testResteasy() throws Exception {
    WebTarget target = client.target(
            TestPortProvider.generateURL(BASE_URI)).path("zip");
    File file = new File("C:/test/test.zip");
    Response response = target.request().post(
            Entity.entity(file, MediaType.APPLICATION_OCTET_STREAM));
    System.out.println(response.getStatus());
    System.out.println(response.readEntity(String.class));
    response.close();
}

Using these files in a zip

test1.xml
---------
<test1>hello world</test1>

test2.xml
---------
<test2>hello squirrel</test2>

test3.json
----------
{
    "test3":"Hello Girls"
}

I get the following result

==== Data ====
name: test1.xml
data: <test1>hello world</test1>
name: test2.xml
data: <test2>hello squirrel</test2>

As an aside, if you have control over how the data is sent, you might want to also look into a multipart solution. There you set content types, and you can name each part, where they're easier to access.

You can see Resteasy's support for multipart here, and the required dependency.


UPDATE

If you must use application/zip, there is no standard support for this. So you would need to whip up your own MessageBodyReader. It could be something as simple as wrapping and return the already provided InputStream

@Provider
@Consumes("application/zip")
public class ZipMessageBodyReader implements MessageBodyReader<ZipInputStream> {

    @Override
    public boolean isReadable(Class<?> type, Type genericType, 
            Annotation[] annotations, MediaType mediaType) {
        return type == ZipInputStream.class;
    }

    @Override
    public ZipInputStream readFrom(Class<ZipInputStream> type, 
            Type genericType, Annotation[] annotations, MediaType mediaType, 
            MultivaluedMap<String, String> httpHeaders, 
            InputStream entityStream) throws IOException, WebApplicationException {

        return new ZipInputStream(entityStream);
    }    
}

Then in your resource method, you could just have a ZipInputStream parameter, instead of InputStream.

@POST
@Consumes("application/zip")
public Response postZipFile(ZipInputStream zip) throws Exception {

On the client side (with the client API), if you were to use application/zip, you would of course need to also write a MessageBodyWriter for application/zip


UPDATE 2

From Comment: I need my method to be able to consume both a simple xml file and a zip archive which contains the xml file, so I annotate the method something like (pseudo code): "consumes(xml, zip)" and declare a method with the parameter InputStream is; In the method body I then need to determine whether this InputStream is of type xml or a zip archive and want to write something similar to: "if(is of type xml) {then treat is as an xml} else {treat is as a zip archive}. Hopefully now the question is more understandable

We can keep your original method signature accepting both application/xml and application/zip. Also we can check which on is actually being sent by injecting HttpHeaders and getting the Content-Type from it. Base on that, we will determine how to extract. Here's another example of how we can complete this

@Path("/zip")
public class ZipResource {

    @POST
    @Consumes({"application/zip", "application/xml"})
    public Response postZipFile(InputStream is, @Context HttpHeaders headers) throws Exception {
        String contentType = headers.getHeaderString(HttpHeaders.CONTENT_TYPE);
        String returnString = null;

        if (null != contentType) switch (contentType) {
            case "application/xml":
                returnString = readXmlFile(is);
                break;
            case "application/zip":
                returnString = readZipFile(is);
                break;
        }

        return Response.ok(returnString).build();
    }

    private String filePartToString(InputStream is, int size) throws Exception {
        String xml;
        byte[] buff = new byte[size];
        is.read(buff, 0, size);
        return new String(buff);
    }

    private String readXmlFile(InputStream is) {
        StringWriter writer = new StringWriter();
        try {
            IOUtils.copy(is, writer, "utf-8");
        } catch (IOException ex) {
            Logger.getLogger(ZipResource.class.getName()).log(Level.SEVERE, null, ex);
        }
        return writer.toString();
    }

    private String readZipFile(InputStream is) {
        StringBuilder builder = new StringBuilder("==== Data ====\n");
        try (ZipInputStream zip = new ZipInputStream(is)) {
            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                String filename = entry.getName();
                if (filename.endsWith(".xml")) {
                    builder.append("name: ").append(entry.getName()).append("\n");
                    String xml = filePartToString(zip, (int) entry.getSize());
                    builder.append("data: ").append(xml).append("\n");
                }
                zip.closeEntry();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return builder.toString();
    }
}

We would need a MessageBodyReader to handle the application/zip type. The one above works fine, but we just need to it return an InputStream instead of ZipInputStream

@Provider
@Consumes("application/zip")
public class ZipMessageBodyReader implements MessageBodyReader<InputStream> {
    @Override
    public boolean isReadable(Class<?> type, Type genericType, 
            Annotation[] annotations, MediaType mediaType) {
        return type == InputStream.class;
    }

    @Override
    public InputStream readFrom(Class<InputStream> type, 
            Type genericType, Annotation[] annotations, MediaType mediaType, 
            MultivaluedMap<String, String> httpHeaders, 
            InputStream entityStream) throws IOException, WebApplicationException {

        return entityStream;
    }  
}

Now with the test

@Test
public void testResteasy() throws Exception {
    WebTarget target = client.target(
            TestPortProvider.generateURL(BASE_URI)).path("zip");


    File file = new File("C:/test/test.zip");
    Response response = target.request().post(
            Entity.entity(file, "application/zip"));

    System.out.println(response.getStatus());
    System.out.println(response.readEntity(String.class));
    response.close();

    file = new File("C:/test/test1.xml");
    response = target.request().post(
            Entity.entity(file, "application/xml"));

    System.out.println(response.getStatus());
    System.out.println(response.readEntity(String.class));
    response.close();

}

we get the following

200
==== Data ====
name: test1.xml
data: <test1>hello world</test1>
name: test2.xml
data: <test2>hello squirrel</test2>

200
<test1>hello world</test1>

Note: With the client, I had to implement a MessageBodyWriter to handle the application/zip type. The following is a simple implementation just to get the test to work. A real implementation would need some fixing

@Provider
@Produces("application/xml")
public class ZipClientMessageBodyWriter implements MessageBodyWriter<File> {

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, 
            Annotation[] annotations, MediaType mediaType) {
        return type == File.class;
    }

    @Override
    public long getSize(File t, Class<?> type, Type genericType, 
            Annotation[] annotations, MediaType mediaType) {
        return -1;
    }

    @Override
    public void writeTo(File t, Class<?> type, Type genericType, 
            Annotation[] annotations, MediaType mediaType, 
            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) 
            throws IOException, WebApplicationException {

        IOUtils.write(IOUtils.toByteArray(new FileInputStream(t)), entityStream);
    }  
}

....

client.register(ZipClientMessageBodyWriter.class);

You'll also note in some of the example code, I made use of Apache Commons IOUtils. Excuse me for that. I was being lazy :-)


UPDATE 3

Actually, we don't need to MessageBodyReader. The algorithm to find the reader will actually just default to the InputStream reader, as it supports application/xml, so it will already return the InputStream whether we have a reader for the application/zip or not

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Thanks for this solution, but how can I determine whether the incoming InputStream is a zip archive with the xml inside or just xml file? – Arthur Eirich Jan 02 '15 at 09:38
  • You can allow `application/zip` but you need to write a `MessageBodyReader`. I orginally had that as part of the answer then removed it. I will update. – Paul Samsotha Jan 02 '15 at 09:40
  • Oh wait, I didn't read the entire comment. Let me ponder for a moment :-) – Paul Samsotha Jan 02 '15 at 09:44
  • Let me explain my question a bit more precisely before you ponder=) – Arthur Eirich Jan 02 '15 at 09:47
  • I need my method to be able to consume both a simple xml file and a zip archive which contains the xml file, so I annotate the method something like (pseudo code): "consumes(xml, zip)" and declare a method with the parameter InputStream is; In the method body I then need to determine whether this InputStream is of type xml or a zip archive and want to write something similar to: "if(is of type xml) {then treat is as an xml} else {treat is as a zip archive}. Hopefully now the question is more understandable – Arthur Eirich Jan 02 '15 at 09:51
  • Will the content-type be one or the other? For instance if `Content-Type: application/zip`, then you can wrap it in the `ZipInputStream`. If `Content-Type: application/xml`, then move forward with using the `InputStream`. You can inject `@Context HttpHeaders` into the method to get the content type – Paul Samsotha Jan 02 '15 at 09:56
  • In which case, you would still need the `MessageBodyReader` to handle the `application/zip` type. Just change the `ZipInputStream` in the `MessageBodyReader` to `InputStream` and simply return the `entityStream` in the `readFrom` method. – Paul Samsotha Jan 02 '15 at 09:59
  • Have a look at my **UPDATE 2**. I codified my last two comments – Paul Samsotha Jan 02 '15 at 10:37
  • Also **UPDATE 3** (read this before doing anything from the second update :-) – Paul Samsotha Jan 02 '15 at 10:46
  • any ideas of how to serialize the incoming input stream and then deserialize it? I could serialize it but trying to deserialize I get the "java.io.StreamCorruptedException: invalid stream header: 3C3F786D". I need the process of serialization to be able to create the xml of the input stream, to modify it and then pack that xml into a wrapping class. – Arthur Eirich Jan 05 '15 at 09:28
  • Are you getting this for the zip file or the xml? Or both? – Paul Samsotha Jan 05 '15 at 09:30
  • At the moment I am testing the variant with the xml – Arthur Eirich Jan 05 '15 at 09:32
  • So you're saying this this exception is occurring before it even hits your resource method? – Paul Samsotha Jan 05 '15 at 09:34
  • No no, the resource method nests another method call, which does the modifying of the file. In my unit test I call this nested method and then get the exception. – Arthur Eirich Jan 05 '15 at 09:37
  • Ahhh.. What is this "wrapping class"? Is it possible maybe that's the problem? Hard to tell without seeing your client code. The example client I have above, everything works fine – Paul Samsotha Jan 05 '15 at 09:41
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/68206/discussion-between-arthur-eirich-and-peeskillet). – Arthur Eirich Jan 05 '15 at 09:42