Flux binaires d'entrée et de sortie utilisant JERSEY?

111

J'utilise Jersey pour implémenter une API RESTful qui récupère et sert principalement des données codées JSON. Mais j'ai des situations dans lesquelles je dois accomplir les tâches suivantes:

  • Exportez des documents téléchargeables, tels que PDF, XLS, ZIP ou autres fichiers binaires.
  • Récupérer des données en plusieurs parties, telles que JSON plus un fichier XLS téléchargé

J'ai un client Web basé sur JQuery d'une seule page qui crée des appels AJAX vers ce service Web. Pour le moment, il ne soumet pas de formulaire et utilise GET et POST (avec un objet JSON). Dois-je utiliser une publication de formulaire pour envoyer des données et un fichier binaire joint, ou puis-je créer une demande en plusieurs parties avec JSON plus un fichier binaire?

La couche de service de mon application crée actuellement un ByteArrayOutputStream lorsqu'elle génère un fichier PDF. Quelle est la meilleure façon de transmettre ce flux au client via Jersey? J'ai créé un MessageBodyWriter, mais je ne sais pas comment l'utiliser à partir d'une ressource Jersey. Est-ce la bonne approche?

J'ai parcouru les exemples inclus avec Jersey, mais je n'ai encore rien trouvé qui illustre comment faire l'une ou l'autre de ces choses. Si cela compte, j'utilise Jersey avec Jackson pour faire Object-> JSON sans l'étape XML et je n'utilise pas vraiment JAX-RS.

Tauren
la source

Réponses:

109

J'ai réussi à obtenir un fichier ZIP ou un fichier PDF en étendant l' StreamingOutputobjet. Voici un exemple de code:

@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 classe PDFGenerator (ma propre classe pour créer le PDF) prend le flux de sortie de la méthode d'écriture et écrit dans celui-ci au lieu d'un flux de sortie nouvellement créé.

Je ne sais pas si c'est la meilleure façon de le faire, mais ça marche.

MikeTheReader
la source
33
Il est également possible de renvoyer le StreamingOutput en tant qu'entité à un Responseobjet. De cette façon, vous pouvez facilement contrôler le mediatype, le code de réponse HTTP, etc. Faites-moi savoir si vous voulez que je poste du code.
Hank
3
@MyTitle: voir exemple
Hank
3
J'ai utilisé les exemples de code de ce fil comme référence et j'ai constaté que je devais vider OutputStream dans StreamingOutput.write () pour que le client reçoive la sortie de manière fiable. Sinon, j'obtiendrais parfois "Content-Length: 0" dans les en-têtes et pas de corps, même si les journaux m'indiquaient que StreamingOutput était en cours d'exécution.
Jon Stewart
@JonStewart - Je crois que je faisais le vidage dans la méthode generatePDF.
MikeTheReader
1
@ Dante617. Souhaitez-vous publier le code côté client de la façon dont le client Jersey envoie le flux binaire au serveur (avec jersey 2.x)?
Débora
29

J'ai dû retourner un fichier rtf et cela a fonctionné pour moi.

// 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
la source
26
Pas terrible, car la sortie n'est envoyée qu'après avoir été complètement préparée. Un octet [] n'est pas un flux.
java.is.for.desktop
7
Cela consomme tous les octets en mémoire, ce qui signifie que des fichiers volumineux peuvent entraîner l'arrêt du serveur. Le but du streaming est d'éviter de consommer tous les octets en mémoire.
Robert Christian
22

J'utilise ce code pour exporter le fichier Excel (xlsx) (Apache Poi) dans jersey en tant que pièce jointe.

@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
la source
15

Voici un autre exemple. Je crée un QRCode au format PNG via un fichier ByteArrayOutputStream. La ressource renvoie un Responseobjet et les données du flux sont l'entité.

Pour illustrer la manipulation du code de réponse, j'ai ajouté le traitement des en- têtes 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;
}   

S'il vous plaît ne me battez pas au cas où il stream.toByteArray()n'y aurait pas de mémoire :) Cela fonctionne pour mes fichiers PNG <1 Ko ...

Hank
la source
6
Je pense que c'est un mauvais exemple de streaming, car l'objet retourné dans la sortie est un tableau d'octets et non un flux.
AlikElzin-kilaka
Bon exemple pour créer une réponse à une demande de ressource GET, pas un bon exemple pour stream. Ce n'est pas du tout un flux.
Robert Christian
14

J'ai composé mes services Jersey 1.17 de la manière suivante:

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

Et le client, si vous en avez besoin:

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
la source
7

Cet exemple montre comment publier des fichiers journaux dans JBoss via une ressource rest. Notez que la méthode get utilise l'interface StreamingOutput pour diffuser le contenu du fichier journal.

@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
la source
1
Juste pour info: au lieu de la méthode pipe, vous pouvez également utiliser IOUtils.copy à partir d'E / S Apache commons.
David
7

Utilisation de Jersey 2.16 Le téléchargement de fichiers est très simple.

Voici l'exemple du fichier 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
la source
1
Fonctionne comme un charme. J'ai une idée de ce contenu en streaming, je ne le comprends pas très bien ...
Oliver
1
C'est le moyen le plus simple si vous utilisez Jersey, merci
ganchito55
Est-il possible de faire avec @POST au lieu de @GET?
spr
@spr Je pense que oui, c'est possible. Lorsque la page du serveur répond, elle devrait fournir la fenêtre de téléchargement
orangegiraffa
5

J'ai trouvé ce qui suit utile pour moi et je voulais partager au cas où cela vous aiderait ou aider quelqu'un d'autre. Je voulais quelque chose comme MediaType.PDF_TYPE, qui n'existe pas, mais ce code fait la même chose:

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

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

Dans mon cas, je publiais un document PDF sur un autre 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")));

Ensuite, p est passé comme deuxième paramètre à post ().

Ce lien m'a été utile pour assembler cet extrait de code: http://jersey.576304.n2.nabble.com/Multipart-Post-td4252846.html

Dovev Hefetz
la source
4

Cela a bien fonctionné avec moi 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
la source
1
Pas sûr Response.ok("file path null").build();, est-ce vraiment ok? Vous devriez probablement utiliser quelque chose commeResponse.status(Status.BAD_REQUEST).entity(...
Christophe Roussy
1

Un autre exemple de code dans lequel vous pouvez télécharger un fichier vers le service REST, le service REST zippe le fichier et le client télécharge le fichier zip à partir du serveur. Ceci est un bon exemple d'utilisation de flux d'entrée et de sortie binaires à l'aide de Jersey.

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

Cette réponse a été publiée par moi dans un autre fil de discussion. J'espère que cela t'aides.

RuntimeException
la source