在RESTful Web服务中使用多个资源

问题描述:

在我的Web服务器应用程序中,我有一个方法,它修改了一个xml文档,看起来类似于:

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
}

消费的zip档案包含需要修改的xml文件和其他一些文件。
如何区分消耗的xml文件和方法内的存档以及我应该使用哪种方法参数来表示此消耗的资源?

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?

一种解决方案是从 InputStream 中读取。您可以将 InputStream 包装在 ZipInputStream 。使用 ZipInputStream ,您可以获得 ZipEntry ZipInputStream.getNextEntry() ,然后你可以用 ZipEntry。的getName() 。然后检查名称 endsWith(。xml)

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").

尽管如此,你需要消耗 application / octet-stream 。这是一个简单的例子

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);
    }
}

这是一个测试

@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"
}

我得到以下结果

==== 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.

你可以看到Resteasy对multipart的支持此处,以及所需的依赖关系

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

如果您必须使用 application / zip ,对此没有标准支持。因此,您需要启动自己的 MessageBodyReader 。它可以像包装一样简单并返回已提供的 InputStream

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);
    }    
}

然后在您的资源方法中,您可以拥有一个 ZipInputStream 参数,而不是 InputStream

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 {

在客户端(使用客户端API),如果您使用 application / zip ,您当然还需要为 application / zip 写$ c> MessageBodyWriter code>

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


来自评论:我需要我的方法能够同时使用简单的xml文件和包含xml文件的zip存档,所以我注释了类似的方法(伪代码) :consume(xml,zip)并使用参数InputStream声明一个方法;在方法体中,我需要确定此InputStream是xml类型还是zip存档,并且想要写类似于:if(类型为xml){then treat is a xml} else {treat is a a zip存档}。希望现在问题更容易理解

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

我们可以保持原始方法签名接受应用程序/ xml application / zip 。我们还可以通过注入 HttpHeaders $ c来检查实际发送的是哪个。 $ c>并从中获取 Content-Type 。基于此,我们将确定如何提取。这是我们如何完成此项的另一个示例

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();
    }
}

我们需要 MessageBodyReader 处理 application / zip 类型。上面的一个工作正常,但我们只需返回 InputStream 而不是 ZipInputStream

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;
    }  
}

现在测试

@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();

}

我们得到以下信息

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

200
<test1>hello world</test1>

注意:对于客户端,我必须实现 MessageBodyWriter 来处理 application / zip 类型。以下是一个简单的实现,只是为了让测试工作。一个真正的实现需要一些修复

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);

您还会在一些示例代码中注意到,我使用了Apache Commons IOUtils 。对不起,请原谅。我很懒: - )

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 :-)

实际上,我们不需要 MessageBodyReader 。查找阅读器的算法实际上只是默认为 InputStream 阅读器,因为它支持 application / xml ,所以它将返回 InputStream 我们是否有 application / zip 的读者

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