javarestdownloadjerseybytestream

How to download a file using a Java REST service and a data stream


I have 3 machines:

  1. server where the file is located
  2. server where REST service is running ( Jersey)
  3. client(browser) with access to 2nd server but no access to 1st server

How can I directly (without saving the file on 2nd server) download the file from 1st server to client's machine?
From 2nd server I can get a ByteArrayOutputStream to get the file from 1st server, can I pass this stream further to the client using the REST service?

It will work this way?

So basically what I want to achieve is to allow the client to download a file from 1st server using the REST service on 2nd server (since there is no direct access from client to 1st server) using only data streams (so no data touching the file system of 2nd server).

What I try now with EasyStream library:

final FTDClient client = FTDClient.getInstance();

try {
    final InputStreamFromOutputStream <String> isOs = new InputStreamFromOutputStream <String>() {
        @Override
        public String produce(final OutputStream dataSink) throws Exception {
            return client.downloadFile2(location, Integer.valueOf(spaceId), URLDecoder.decode(filePath, "UTF-8"), dataSink);
        }
    };
    try {
        String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);

        StreamingOutput output = new StreamingOutput() {
            @Override
            public void write(OutputStream outputStream) throws IOException, WebApplicationException {
                int length;
                byte[] buffer = new byte[1024];
                while ((length = isOs.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, length);
                }
                outputStream.flush();
            }
        };
        return Response.ok(output, MediaType.APPLICATION_OCTET_STREAM)
            .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
            .build();
    }
}

UPDATE2

So my code now with the custom MessageBodyWriter looks simple:

ByteArrayOutputStream baos = new ByteArrayOutputStream(2048) ;
client.downloadFile(location, spaceId, filePath, baos);
return Response.ok(baos).build();

But I get the same heap error when trying with large files.

UPDATE3 Finally managed to get it working ! StreamingOutput did the trick.

Thank you @peeskillet ! Many thanks !


Solution

  • "How can I directly (without saving the file on 2nd server) download the file from 1st server to client's machine?"

    Just use the Client API and get the InputStream from the response

    Client client = ClientBuilder.newClient();
    String url = "...";
    final InputStream responseStream = client.target(url).request().get(InputStream.class);
    

    There are two flavors to get the InputStream. You can also use

    Response response = client.target(url).request().get();
    InputStream is = (InputStream)response.getEntity();
    

    Which one is the more efficient? I'm not sure, but the returned InputStreams are different classes, so you may want to look into that if you care to.

    From 2nd server I can get a ByteArrayOutputStream to get the file from 1st server, can I pass this stream further to the client using the REST service?

    So most of the answers you'll see in the link provided by @GradyGCooper seem to favor the use of StreamingOutput. An example implementation might be something like

    final InputStream responseStream = client.target(url).request().get(InputStream.class);
    System.out.println(responseStream.getClass());
    StreamingOutput output = new StreamingOutput() {
        @Override
        public void write(OutputStream out) throws IOException, WebApplicationException {  
            int length;
            byte[] buffer = new byte[1024];
            while((length = responseStream.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            out.flush();
            responseStream.close();
        }   
    };
    return Response.ok(output).header(
            "Content-Disposition", "attachment, filename=\"...\"").build();
    

    But if we look at the source code for StreamingOutputProvider, you'll see in the writeTo, that it simply writes the data from one stream to another. So with our implementation above, we have to write twice.

    How can we get only one write? Simple return the InputStream as the Response

    final InputStream responseStream = client.target(url).request().get(InputStream.class);
    return Response.ok(responseStream).header(
            "Content-Disposition", "attachment, filename=\"...\"").build();
    

    If we look at the source code for InputStreamProvider, it simply delegates to ReadWriter.writeTo(in, out), which simply does what we did above in the StreamingOutput implementation

     public static void writeTo(InputStream in, OutputStream out) throws IOException {
        int read;
        final byte[] data = new byte[BUFFER_SIZE];
        while ((read = in.read(data)) != -1) {
            out.write(data, 0, read);
        }
    }
    

    Asides:


    UPDATE

    I'm such a dufus. While the OP and I are contemplating ways to turn a ByteArrayOutputStream to an InputStream, I missed the simplest solution, which is simply to write a MessageBodyWriter for the ByteArrayOutputStream

    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Type;
    import javax.ws.rs.WebApplicationException;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.MultivaluedMap;
    import javax.ws.rs.ext.MessageBodyWriter;
    import javax.ws.rs.ext.Provider;
    
    @Provider
    public class OutputStreamWriter implements MessageBodyWriter<ByteArrayOutputStream> {
    
        @Override
        public boolean isWriteable(Class<?> type, Type genericType,
                Annotation[] annotations, MediaType mediaType) {
            return ByteArrayOutputStream.class == type;
        }
    
        @Override
        public long getSize(ByteArrayOutputStream t, Class<?> type, Type genericType,
                Annotation[] annotations, MediaType mediaType) {
            return -1;
        }
    
        @Override
        public void writeTo(ByteArrayOutputStream t, Class<?> type, Type genericType,
                Annotation[] annotations, MediaType mediaType,
                MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
                throws IOException, WebApplicationException {
            t.writeTo(entityStream);
        }
    }
    

    Then we can simply return the ByteArrayOutputStream in the response

    return Response.ok(baos).build();
    

    D'OH!

    UPDATE 2

    Here are the tests I used (

    Resource class

    @Path("test")
    public class TestResource {
    
        final String path = "some_150_mb_file";
    
        @GET
        @Produces(MediaType.APPLICATION_OCTET_STREAM)
        public Response doTest() throws Exception {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int len;
            byte[] buffer = new byte[4096];
            while ((len = is.read(buffer, 0, buffer.length)) != -1) {
                baos.write(buffer, 0, len);
            }
            System.out.println("Server size: " + baos.size());
            return Response.ok(baos).build();
        }
    }
    

    Client test

    public class Main {
        public static void main(String[] args) throws Exception {
            Client client = ClientBuilder.newClient();
            String url = "http://localhost:8080/api/test";
            Response response = client.target(url).request().get();
            String location = "some_location";
            FileOutputStream out = new FileOutputStream(location);
            InputStream is = (InputStream)response.getEntity();
            int len = 0;
            byte[] buffer = new byte[4096];
            while((len = is.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            out.flush();
            out.close();
            is.close();
        }
    }
    

    UPDATE 3

    So the final solution for this particular use case was for the OP to simply pass the OutputStream from the StreamingOutput's write method. Seems the third-party API, required a OutputStream as an argument.

    StreamingOutput output = new StreamingOutput() {
        @Override
        public void write(OutputStream out) {
            thirdPartyApi.downloadFile(.., .., .., out);
        }
    }
    return Response.ok(output).build();
    

    Not quite sure, but seems the reading/writing within the resource method, using ByteArrayOutputStream`, realized something into memory.

    The point of the downloadFile method accepting an OutputStream is so that it can write the result directly to the OutputStream provided. For instance a FileOutputStream, if you wrote it to file, while the download is coming in, it would get directly streamed to the file.

    It's not meant for us to keep a reference to the OutputStream, as you were trying to do with the baos, which is where the memory realization comes in.

    So with the way that works, we are writing directly to the response stream provided for us. The method write doesn't actually get called until the writeTo method (in the MessageBodyWriter), where the OutputStream is passed to it.

    You can get a better picture looking at the MessageBodyWriter I wrote. Basically in the writeTo method, replace the ByteArrayOutputStream with StreamingOutput, then inside the method, call streamingOutput.write(entityStream). You can see the link I provided in the earlier part of the answer, where I link to the StreamingOutputProvider. This is exactly what happens