Download xml from REST service using jax-rs withou

2019-07-01 16:56发布

In a service, I am creating an XML document named doc and I want the user to receive a prompt to download the document without having to save it locally (like the one that says open or save the file).

However, I am not able to find how I should build the response that is going to be returned, or even what type to @produce.

So far I have this:

@GET
@Path("/getXML")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public StreamingOutput getXML(
        @FormParam("id") int id) {
    UserDB userDao = new UserDB();
    entities.User userd = userDao.getById(id);

    DocumentBuilderFactory icFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder icBuilder;

    try {
        icBuilder = icFactory.newDocumentBuilder();
        Document doc = icBuilder.newDocument();

        Element rootElement = doc.createElement("Users");
        doc.appendChild(rootElement);

        rootElement.appendChild(getUser(doc, "1", "asd", "adas"));
        rootElement.appendChild(getUser(doc, "2", "bbb", "ccc"));

        //Here I should return the doc that is going to be downloaded
    }
    catch (Exception e) {
        e.printStackTrace();
    }

}

EDIT1: My main problem is that I cannot find how to build the response that is going to be returned. The answers that I have found download an already existing file that is locally stored.

The thread that is closest on answering is: How to make an XML document downloadable without intermediate file storage?

but I cannot understand how to apply it on my REST service response which differs from the HttpServletResponse.

1条回答
放我归山
2楼-- · 2019-07-01 17:44

If you look at the answer you linked to, you will see that a StreamResult is used. In the answer, a StringWriter is passed to the constructor, but if you look at the Javadoc, it has an overloaded constructor that also takes an OutputStream. So if you're returning a StreamingOutput, just pass the OutputStream from the StreamingOutput#write(OutputStream) method to the StreamResult constructor. Everything else from the answer should be the same.

return new StreamingOutput() {
    @Override
    public void write(OutputStream out)
            throws IOException, WebApplicationException {
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            StreamResult result = new StreamResult(out);
            DOMSource source = new DOMSource(doc);
            transformer.transform(source, result);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};

Here's the complete resource class I used to test. Notice that I use @Produces(MediaType.APPLICATION_XML). There's no point in setting to application/octet-stream if the data is XML1.

@Path("dom")
public class DomXmlResource {

    @GET
    @Produces(MediaType.APPLICATION_XML)
    public StreamingOutput getXml() throws Exception {

        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

        Document doc = docBuilder.newDocument();
        Element rootElement = doc.createElement("company");
        doc.appendChild(rootElement);

        Element staff = doc.createElement("Staff");
        rootElement.appendChild(staff);

        staff.setAttribute("id", "1");

        Element firstname = doc.createElement("firstname");
        firstname.appendChild(doc.createTextNode("yong"));
        staff.appendChild(firstname);

        return new StreamingOutput() {
            @Override
            public void write(OutputStream out)
                    throws IOException, WebApplicationException {
                try {
                    Transformer transformer = TransformerFactory.newInstance()
                            .newTransformer();
                    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                    StreamResult result = new StreamResult(out);
                    DOMSource source = new DOMSource(doc);
                    transformer.transform(source, result);
                    out.flush();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
    }
}

Update

To automatically download the file (as opposed to displaying the XML result), we actually need to add the Content-Disposition header with the attachment value. To do that, instead of returning StreamingOutput from the method, we should return Response, where the entity will be the StreamingOutput

@Path("dom")
public class DomXmlResource {

    @GET
    @Produces(MediaType.APPLICATION_XML)
    public Response getXml() throws Exception {
        ...
        StreamingOutput entity = new StreamingOutput() {
            @Override
            public void write(OutputStream out)
                    throws IOException, WebApplicationException {
                ...
            }
        };
        return Response.ok(entity)
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                        "attachment;filename=file.xml")
                .build();
    }
}

Update 2

If you don't already know, you can simply return your POJOs (or list of them) and they will automatically get serialized to XML. You don't need to manually use the DOM classes to create XML structure. There are already Entity Providers that will handle the conversion from POJO to XML for us. For example, if we have the following POJO (needs to be annotated with @XmlRootElement)

@XmlRootElement
public class User {
    private String name;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

then we can just return it and it will automatically be serialized to

<user><name>footer</name></user>

Here's an example

@Path("pojo")
public class PojoXmlResource {

    @GET
    @Produces("application/xml")
    public Response getXml() {
        User user = new User();
        user.setName("Jane Doe");

        return Response.ok(user)
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment;filename=user.xml")
                .build();
    }
}

It's a lot less messy isn't it? If you want to return a list of Users, then you need to wrap it in GenericEntity

List<User> users = Arrays.asList(user1, user2, user3);
GenericEntity<List<User>> entity = new GenericEntity<List<User>>(users){};
return Response.ok(entity)
        ...
        .build();

1. See: Do I need Content-Type: application/octet-stream for file download?

查看更多
登录 后发表回答