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:
- 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.
- 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.
- 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:
- HttpServletResponse should be one of the parameters your void method receives
- Read the required file from the disk (in the example above it was already read in previous call)
- 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)
- Call response.flushBuffer() to complete the streaming
Hope it helps.