Saturday, June 28, 2014

Stream a file using Spring MVC Controller

Did you ever need a way to stream a file directly to the browser as a result of calling a specific URI?

I did, more than once. Every time it requires some time to recall what is the correct way to do that.
This post will be a reminder for me and might become useful for anyone who reads this.

Why would I want to do that?

I needed that in following scenarios:

  1. I've just generated a pdf file and I'm required to return it to web based client, so the pdf file would be opened or downloaded automatically by the browser.
  2. There is a bunch of  image files located in some custom (preferably predefined) location that is not exposed  via direct URL, a.k.a it's not located in any subfolder under WEB-INF directory. This configuration is useful when I need to allow a customer put some custom icons without giving him an access to my web application resources or implementing file upload.
  3. In general: any time I need to return to web browser any downloadable type of file or file type that browsers are able to open.

How to do that?


Let's define a simple Spring MVC controller. In order access to application context I'll implement ApplicationContextAware interface.

@Controller
@RequestMapping("/customIcons")
public class CustomIconsController  implements ApplicationContextAware {
    private ApplicationContext applicationContext;
       
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

In this specific implementation I needed a way to enlist all the images under specific location. Once a caller UI gets them, it will have to show a dialog with all the available icons and will allow the user to pick one of the presented icons.


    // will contain all the found resources in order to return the stream as soon as the request arrives
    private Map foundResources = new HashMap<>(); 

    @RequestMapping("/all")
    @ResponseBody
    public Collection getAllCustomIcons() {

        Collection retVal = new ArrayList<>();
        Resource[] resources = null;

        try {
            // getting all the .png files from predefined location 
            resources= applicationContext.getResources("file:c:/customIconsPath/*.png");
        } catch (IOException e) {
            // any fallback solution can come here, for example: try to load images from your classpath instead 
            throw new ConfigurationException("No custom icons directory available");
        }

        extractFileNames(retVal, resources);

        return retVal;

    }
    private void extractFileNames(Collection retVal, Resource[] resources) {
        if (resources != null) {
            for (Resource resource : resources) {
                String finalName = resource.getFilename().replace(".png", "");// just removing the extention
                if (!"".equals(finalName)) {
                    retVal.add(finalName);
                    foundResources.put(resource.getFilename(), resource);
                }
            }
        }
    }

So far so good, I've got the list of all my .png files and now I want to show them in html using img tag Using AngularJS I would write something like this:

<img ng-repeat="customIconName in customIconNames"  ng-src="'customIcons/icon/' + customIconName"/>    

 You can use any other JS technique to get the resulting file name from the list returned by previous call.

 And that's the way method returning the requested image will look:


    @RequestMapping("/icon/{fileName}")
    // pay attention to second parameter, Spring MVC can pass HTTP response and request besides the path variable parameters
    public void getCustomIcon(@PathVariable("fileName") final String fileName, HttpServletResponse response) {

        try {
            // we've returned the filename without extension, so we put the extension back
            // alternatively we could save the Map key without the extension.    
            InputStream inputStream = foundResources.get(fileName + ".png").getInputStream();
            // the browser should know what kind of data are we streaming so setting a mime type is essential  
            response.setContentType("image/png");
            // copy the file input stream to response output stream (see the function bellow)
            writeStream(inputStream, response.getOutputStream());
            // flushing the response
            response.flushBuffer();

        } catch (FileNotFoundException e) {
            // just logging the errors
            logger.error(e);
        } catch (IOException e) {
            logger.error(e);
        }
   }

   public static void writeStream(InputStream inputStream, OutputStream outputStream) throws IOException {
        int buf_size = 8192;
        byte[] buf = new byte[buf_size];
        int len;
        while ((len = inputStream.read(buf, 0, buf_size)) != -1) {
            outputStream.write(buf, 0, len);
        }
    }

The getCustomIcon method, which is the whole reason for this post, is very simple  and requires to follow several steps:

  1. HttpServletResponse should be one of the parameters your void method receives
  2. Read the required file from the disk (in the example above it was already read in previous call)
  3. Copy  the input stream from of your file to response output stream (I've just used the fact that Resource can return an input stream right away)
  4. Call response.flushBuffer() to  complete the streaming
Hope it helps.