Fluxos binários de entrada e saída usando JERSEY?

111

Estou usando Jersey para implementar uma API RESTful que basicamente recupera e fornece dados codificados em JSON. Mas tenho algumas situações em que preciso realizar o seguinte:

  • Exporte documentos para download, como PDF, XLS, ZIP ou outros arquivos binários.
  • Recupere dados multipartes, como JSON mais um arquivo XLS carregado

Eu tenho um cliente da web baseado em JQuery de página única que cria chamadas AJAX para este serviço da web. No momento, ele não envia formulários e usa GET e POST (com um objeto JSON). Devo utilizar uma postagem de formulário para enviar dados e um arquivo binário anexado ou posso criar uma solicitação multiparte com JSON mais o arquivo binário?

A camada de serviço do meu aplicativo atualmente cria um ByteArrayOutputStream ao gerar um arquivo PDF. Qual é a melhor maneira de enviar esse stream para o cliente via Jersey? Criei um MessageBodyWriter, mas não sei como usá-lo de um recurso de Jersey. Essa é a abordagem certa?

Estive olhando os exemplos incluídos em Jersey, mas ainda não encontrei nada que ilustre como fazer qualquer uma dessas coisas. Se for importante, estou usando Jersey com Jackson para fazer Object-> JSON sem a etapa XML e não estou realmente utilizando JAX-RS.

Tauren
fonte

Respostas:

109

Consegui obter um arquivo ZIP ou PDF estendendo o StreamingOutputobjeto. Aqui está um exemplo de código:

@Path("PDF-file.pdf/")
@GET
@Produces({"application/pdf"})
public StreamingOutput getPDF() throws Exception {
    return new StreamingOutput() {
        public void write(OutputStream output) throws IOException, WebApplicationException {
            try {
                PDFGenerator generator = new PDFGenerator(getEntity());
                generator.generatePDF(output);
            } catch (Exception e) {
                throw new WebApplicationException(e);
            }
        }
    };
}

A classe PDFGenerator (minha própria classe para criar o PDF) pega o fluxo de saída do método de gravação e grava nele em vez de um fluxo de saída recém-criado.

Não sei se é a melhor maneira de fazer isso, mas funciona.

MikeTheReader
fonte
33
Também é possível retornar o StreamingOutput como a entidade para um Responseobjeto. Dessa forma, você pode controlar facilmente o tipo de mídia, o código de resposta HTTP, etc. Deixe-me saber se você deseja que eu poste o código.
Hank
3
@MyTitle: ver exemplo
Hank
3
Usei os exemplos de código neste segmento como referência e descobri que precisava liberar o OutputStream em StreamingOutput.write () para que o cliente recebesse a saída de forma confiável. Caso contrário, às vezes eu obteria "Content-Length: 0" nos cabeçalhos e nenhum corpo, embora os logs me informassem que o StreamingOutput estava em execução.
Jon Stewart
@JonStewart - Acredito que estava fazendo o flush dentro do método generatePDF.
MikeTheReader
1
@ Dante617. Você postaria o código do lado do cliente como o cliente Jersey envia o fluxo binário para o servidor (com jersey 2.x)?
Débora
29

Eu tive que retornar um arquivo rtf e isso funcionou para mim.

// create a byte array of the file in correct format
byte[] docStream = createDoc(fragments); 

return Response
            .ok(docStream, MediaType.APPLICATION_OCTET_STREAM)
            .header("content-disposition","attachment; filename = doc.rtf")
            .build();
Abhishek Rakshit
fonte
26
Não tão bom, porque a saída só é enviada depois de estar completamente preparada. Um byte [] não é um fluxo.
java.is.for.desktop
7
Isso consome todos os bytes na memória, o que significa que arquivos grandes podem derrubar o servidor. O objetivo do streaming é evitar o consumo de todos os bytes na memória.
Robert Christian
22

Estou usando este código para exportar o arquivo excel (xlsx) (Apache Poi) em jersey como um anexo.

@GET
@Path("/{id}/contributions/excel")
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public Response exportExcel(@PathParam("id") Long id)  throws Exception  {

    Resource resource = new ClassPathResource("/xls/template.xlsx");

    final InputStream inp = resource.getInputStream();
    final Workbook wb = WorkbookFactory.create(inp);
    Sheet sheet = wb.getSheetAt(0);

    Row row = CellUtil.getRow(7, sheet);
    Cell cell = CellUtil.getCell(row, 0);
    cell.setCellValue("TITRE TEST");

    [...]

    StreamingOutput stream = new StreamingOutput() {
        public void write(OutputStream output) throws IOException, WebApplicationException {
            try {
                wb.write(output);
            } catch (Exception e) {
                throw new WebApplicationException(e);
            }
        }
    };


    return Response.ok(stream).header("content-disposition","attachment; filename = export.xlsx").build();

}
Grégory
fonte
15

Aqui está outro exemplo. Estou criando um QRCode como PNG por meio de um ByteArrayOutputStream. O recurso retorna umResponse objeto e os dados do fluxo são a entidade.

Para ilustrar a manipulação de código de resposta, adicionei manuseamento de cabeçalhos de cache ( If-modified-since, If-none-matches, etc.).

@Path("{externalId}.png")
@GET
@Produces({"image/png"})
public Response getAsImage(@PathParam("externalId") String externalId, 
        @Context Request request) throws WebApplicationException {

    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    // do something with externalId, maybe retrieve an object from the
    // db, then calculate data, size, expirationTimestamp, etc

    try {
        // create a QRCode as PNG from data     
        BitMatrix bitMatrix = new QRCodeWriter().encode(
                data, 
                BarcodeFormat.QR_CODE, 
                size, 
                size
        );
        MatrixToImageWriter.writeToStream(bitMatrix, "png", stream);

    } catch (Exception e) {
        // ExceptionMapper will return HTTP 500 
        throw new WebApplicationException("Something went wrong …")
    }

    CacheControl cc = new CacheControl();
    cc.setNoTransform(true);
    cc.setMustRevalidate(false);
    cc.setNoCache(false);
    cc.setMaxAge(3600);

    EntityTag etag = new EntityTag(HelperBean.md5(data));

    Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(
            updateTimestamp,
            etag
    );
    if (responseBuilder != null) {
        // Preconditions are not met, returning HTTP 304 'not-modified'
        return responseBuilder
                .cacheControl(cc)
                .build();
    }

    Response response = Response
            .ok()
            .cacheControl(cc)
            .tag(etag)
            .lastModified(updateTimestamp)
            .expires(expirationTimestamp)
            .type("image/png")
            .entity(stream.toByteArray())
            .build();
    return response;
}   

Por favor, não me bata no caso de stream.toByteArray()não haver memória :) Funciona para meus arquivos PNG <1 KB ...

Hank
fonte
6
Acho que é um exemplo de streaming ruim, já que o objeto retornado na saída é uma matriz de bytes e não um stream.
AlikElzin-kilaka,
Bom exemplo para construir uma resposta a uma solicitação de recurso GET, não é um bom exemplo para fluxo. Este não é um riacho.
Robert Christian
14

Tenho redigido meus serviços Jersey 1.17 da seguinte maneira:

FileStreamingOutput

public class FileStreamingOutput implements StreamingOutput {

    private File file;

    public FileStreamingOutput(File file) {
        this.file = file;
    }

    @Override
    public void write(OutputStream output)
            throws IOException, WebApplicationException {
        FileInputStream input = new FileInputStream(file);
        try {
            int bytes;
            while ((bytes = input.read()) != -1) {
                output.write(bytes);
            }
        } catch (Exception e) {
            throw new WebApplicationException(e);
        } finally {
            if (output != null) output.close();
            if (input != null) input.close();
        }
    }

}

GET

@GET
@Produces("application/pdf")
public StreamingOutput getPdf(@QueryParam(value="name") String pdfFileName) {
    if (pdfFileName == null)
        throw new WebApplicationException(Response.Status.BAD_REQUEST);
    if (!pdfFileName.endsWith(".pdf")) pdfFileName = pdfFileName + ".pdf";

    File pdf = new File(Settings.basePath, pdfFileName);
    if (!pdf.exists())
        throw new WebApplicationException(Response.Status.NOT_FOUND);

    return new FileStreamingOutput(pdf);
}

E o cliente, se precisar:

Client

private WebResource resource;

public InputStream getPDFStream(String filename) throws IOException {
    ClientResponse response = resource.path("pdf").queryParam("name", filename)
        .type("application/pdf").get(ClientResponse.class);
    return response.getEntityInputStream();
}
Daniel Szalay
fonte
7

Este exemplo mostra como publicar arquivos de log no JBoss por meio de um recurso rest. Observe que o método get usa a interface StreamingOutput para transmitir o conteúdo do arquivo de log.

@Path("/logs/")
@RequestScoped
public class LogResource {

private static final Logger logger = Logger.getLogger(LogResource.class.getName());
@Context
private UriInfo uriInfo;
private static final String LOG_PATH = "jboss.server.log.dir";

public void pipe(InputStream is, OutputStream os) throws IOException {
    int n;
    byte[] buffer = new byte[1024];
    while ((n = is.read(buffer)) > -1) {
        os.write(buffer, 0, n);   // Don't allow any extra bytes to creep in, final write
    }
    os.close();
}

@GET
@Path("{logFile}")
@Produces("text/plain")
public Response getLogFile(@PathParam("logFile") String logFile) throws URISyntaxException {
    String logDirPath = System.getProperty(LOG_PATH);
    try {
        File f = new File(logDirPath + "/" + logFile);
        final FileInputStream fStream = new FileInputStream(f);
        StreamingOutput stream = new StreamingOutput() {
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException {
                try {
                    pipe(fStream, output);
                } catch (Exception e) {
                    throw new WebApplicationException(e);
                }
            }
        };
        return Response.ok(stream).build();
    } catch (Exception e) {
        return Response.status(Response.Status.CONFLICT).build();
    }
}

@POST
@Path("{logFile}")
public Response flushLogFile(@PathParam("logFile") String logFile) throws URISyntaxException {
    String logDirPath = System.getProperty(LOG_PATH);
    try {
        File file = new File(logDirPath + "/" + logFile);
        PrintWriter writer = new PrintWriter(file);
        writer.print("");
        writer.close();
        return Response.ok().build();
    } catch (Exception e) {
        return Response.status(Response.Status.CONFLICT).build();
    }
}    

}

Jaime Casero
fonte
1
Apenas para sua informação: em vez do método pipe, você também pode usar IOUtils.copy do Apache commons I / O.
David
7

Usar o Jersey 2.16 é muito fácil baixar arquivos.

Abaixo está o exemplo para o arquivo ZIP

@GET
@Path("zipFile")
@Produces("application/zip")
public Response getFile() {
    File f = new File(ZIP_FILE_PATH);

    if (!f.exists()) {
        throw new WebApplicationException(404);
    }

    return Response.ok(f)
            .header("Content-Disposition",
                    "attachment; filename=server.zip").build();
}
orangegiraffa
fonte
1
Funciona como um encanto. Tem alguma ideia sobre essas coisas de streaming, eu não entendo muito bem ...
Oliver
1
É a maneira mais fácil se você usar Jersey, obrigado
ganchito55
É possível fazer com @POST em vez de @GET?
spr
@spr acho que sim, é possível. Quando a página do servidor responder, ela deverá fornecer a janela de download
orangegiraffa
5

Achei o seguinte útil para mim e gostaria de compartilhar, caso isso ajude você ou outra pessoa. Eu queria algo como MediaType.PDF_TYPE, que não existe, mas este código faz a mesma coisa:

DefaultMediaTypePredictor.CommonMediaTypes.
        getMediaTypeFromFileName("anything.pdf")

Vejo http://jersey.java.net/nonav/apidocs/1.1.0-ea/contribs/jersey-multipart/com/sun/jersey/multipart/file/DefaultMediaTypePredictor.CommonMediaTypes.html

No meu caso, estava postando um documento PDF em outro site:

FormDataMultiPart p = new FormDataMultiPart();
p.bodyPart(new FormDataBodyPart(FormDataContentDisposition
        .name("fieldKey").fileName("document.pdf").build(),
        new File("path/to/document.pdf"),
        DefaultMediaTypePredictor.CommonMediaTypes
                .getMediaTypeFromFileName("document.pdf")));

Então p é passado como o segundo parâmetro para post ().

Este link foi útil para eu juntar este snippet de código: http://jersey.576304.n2.nabble.com/Multipart-Post-td4252846.html

Dovev Hefetz
fonte
4

Isso funcionou bem comigo url: http://example.com/rest/muqsith/get-file?filePath=C : \ Users \ I066807 \ Desktop \ test.xml

@GET
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
@Path("/get-file")
public Response getFile(@Context HttpServletRequest request){
   String filePath = request.getParameter("filePath");
   if(filePath != null && !"".equals(filePath)){
        File file = new File(filePath);
        StreamingOutput stream = null;
        try {
        final InputStream in = new FileInputStream(file);
        stream = new StreamingOutput() {
            public void write(OutputStream out) throws IOException, WebApplicationException {
                try {
                    int read = 0;
                        byte[] bytes = new byte[1024];

                        while ((read = in.read(bytes)) != -1) {
                            out.write(bytes, 0, read);
                        }
                } catch (Exception e) {
                    throw new WebApplicationException(e);
                }
            }
        };
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
        return Response.ok(stream).header("content-disposition","attachment; filename = "+file.getName()).build();
        }
    return Response.ok("file path null").build();
}
Muqsith
fonte
1
Não tenho certeza Response.ok("file path null").build();, está tudo bem? Você provavelmente deve usar algo comoResponse.status(Status.BAD_REQUEST).entity(...
Christophe Roussy
1

Outro código de amostra em que você pode fazer upload de um arquivo para o serviço REST, o serviço REST compacta o arquivo e o cliente baixa o arquivo compactado do servidor. Este é um bom exemplo de como usar fluxos de entrada e saída binários usando Jersey.

https://stackoverflow.com/a/32253028/15789

Esta resposta foi postada por mim em outro tópico. Espero que isto ajude.

Exceção de tempo de execução
fonte