¿Flujos binarios de entrada y salida usando JERSEY?

111

Estoy usando Jersey para implementar una API RESTful que principalmente recupera y sirve datos codificados JSON. Pero tengo algunas situaciones en las que necesito lograr lo siguiente:

  • Exporte documentos descargables, como PDF, XLS, ZIP u otros archivos binarios.
  • Recupere datos de varias partes, como JSON más un archivo XLS cargado

Tengo un cliente web basado en JQuery de una sola página que crea llamadas AJAX a este servicio web. Por el momento, no envía formularios y usa GET y POST (con un objeto JSON). ¿Debo utilizar una publicación de formulario para enviar datos y un archivo binario adjunto, o puedo crear una solicitud de varias partes con JSON más un archivo binario?

La capa de servicio de mi aplicación crea actualmente un ByteArrayOutputStream cuando genera un archivo PDF. ¿Cuál es la mejor manera de enviar este flujo al cliente a través de Jersey? Creé un MessageBodyWriter, pero no sé cómo usarlo desde un recurso de Jersey. ¿Ese es el enfoque correcto?

He estado mirando las muestras incluidas con Jersey, pero todavía no he encontrado nada que ilustre cómo hacer ninguna de estas cosas. Si importa, estoy usando Jersey con Jackson para hacer Object-> JSON sin el paso XML y realmente no estoy utilizando JAX-RS.

Tauren
fuente

Respuestas:

109

Logré obtener un archivo ZIP o un archivo PDF al extender el StreamingOutputobjeto. Aquí hay un código de muestra:

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

La clase PDFGenerator (mi propia clase para crear el PDF) toma el flujo de salida del método de escritura y lo escribe en lugar de un flujo de salida recién creado.

No sé si es la mejor manera de hacerlo, pero funciona.

MikeTheReader
fuente
33
También es posible devolver StreamingOutput como entidad a un Responseobjeto. De esa manera, puede controlar fácilmente el tipo de medio, el código de respuesta HTTP, etc. Avíseme si desea que publique el código.
Hank
3
@MyTitle: ver ejemplo
Hank
3
Usé los ejemplos de código en este hilo como referencia y descubrí que necesitaba vaciar OutputStream en StreamingOutput.write () para que el cliente reciba la salida de manera confiable. De lo contrario, a veces obtenía "Content-Length: 0" en los encabezados y ningún cuerpo, aunque los registros me indicaban que StreamingOutput se estaba ejecutando.
Jon Stewart
@JonStewart: creo que estaba haciendo el vaciado dentro del método generatePDF.
MikeTheReader
1
@ Dante617. ¿Publicaría el código del lado del cliente cómo el cliente de Jersey envía el flujo binario al servidor (con jersey 2.x)?
Débora
29

Tuve que devolver un archivo rtf y esto funcionó para mí.

// 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
fuente
26
No es tan bueno, porque la salida se envía solo después de estar completamente preparada. Un byte [] no es una secuencia.
java.is.for.desktop
7
Esto consume todos los bytes de la memoria, lo que significa que los archivos grandes podrían hacer que el servidor caiga. El propósito de la transmisión es evitar consumir todos los bytes en la memoria.
Robert Christian
22

Estoy usando este código para exportar el archivo de Excel (xlsx) (Apache Poi) en jersey como adjunto.

@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
fuente
15

He aquí otro ejemplo. Estoy creando un QRCode como PNG a través de un archivo ByteArrayOutputStream. El recurso devuelve unResponse objeto y los datos de la secuencia son la entidad.

Para ilustrar el manejo código de respuesta, he añadido el manejo de cabeceras de caché ( 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, no me golpeen en caso de que no stream.toByteArray()tenga memoria :) Funciona para mis archivos PNG de <1KB ...

Madeja
fuente
6
Creo que es un mal ejemplo de transmisión, ya que el objeto devuelto en la salida es una matriz de bytes y no una transmisión.
AlikElzin-kilaka
Buen ejemplo de creación de una respuesta a una solicitud de recursos GET, no un buen ejemplo de transmisión. Esto no es una corriente en absoluto.
Robert Christian
14

He estado componiendo mis servicios de Jersey 1.17 de la siguiente manera:

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

Y el cliente, si lo necesita:

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
fuente
7

Este ejemplo muestra cómo publicar archivos de registro en JBoss a través de un recurso de descanso. Tenga en cuenta que el método get utiliza la interfaz StreamingOutput para transmitir el contenido del archivo de registro.

@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
fuente
1
Solo para su información: en lugar del método de tubería, también puede usar IOUtils.copy de Apache commons I / O.
David
7

Usar Jersey 2.16 La descarga de archivos es muy fácil.

A continuación se muestra el ejemplo del archivo 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
fuente
1
Funciona de maravilla. Tengo alguna idea sobre este material de transmisión, no lo entiendo del todo ...
Oliver
1
Es la forma más fácil si usa Jersey, gracias
ganchito55
¿Es posible hacerlo con @POST en lugar de @GET?
SPR
@spr Creo que sí, es posible. Cuando la página del servidor responde, debería proporcionar la ventana de descarga
orangegiraffa
5

Encontré lo siguiente útil para mí y quería compartirlo en caso de que le ayude a usted oa otra persona. Quería algo como MediaType.PDF_TYPE, que no existe, pero este código hace lo mismo:

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

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

En mi caso, estaba publicando un documento PDF en otro sitio:

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

Luego, p se pasa como segundo parámetro a post ().

Este enlace me resultó útil al armar este fragmento de código: http://jersey.576304.n2.nabble.com/Multipart-Post-td4252846.html

Dovev Hefetz
fuente
4

Esto funcionó bien conmigo 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
fuente
1
No estoy seguro Response.ok("file path null").build();, ¿está realmente bien? Probablemente debería usar algo comoResponse.status(Status.BAD_REQUEST).entity(...
Christophe Roussy
1

Otro código de muestra donde puede cargar un archivo al servicio REST, el servicio REST comprime el archivo y el cliente descarga el archivo zip del servidor. Este es un buen ejemplo del uso de flujos de entrada y salida binarios con Jersey.

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

Esta respuesta fue publicada por mí en otro hilo. Espero que esto ayude.

Excepción en tiempo de ejecución
fuente